ADR-0024 Phase D: include column name in value-slot hint prose
User-facing improvement: typing into a value slot now surfaces
the column name in the hint. The hint at `insert into Customers
values (` (first column id:int) reads "for `id`: Type an
integer (e.g. 42, -7) or null" instead of the generic
"Type an integer …" prose. After `1, ` the panel updates to
the second column ("for `Name`: Type a quoted string …"). The
same applies to `update T set Email=` and `delete from T where
ts=` — the catalog wrapper threads the column name through.
Implementation:
**`Node::TypedValueSlot.column_name: Option<&'static str>`**
(new field, `src/dsl/grammar/mod.rs`). When `Some`, walker
writes `WalkContext::pending_value_column` on entry; clears
along with `pending_value_type` on inner success.
**Walker driver writes both names** (`src/dsl/walker/driver.rs`):
- `Node::TypedValueSlot` dispatch reads `column_name` and
populates `pending_value_column`.
- `Ident { writes_column: true }` dispatch also writes
`pending_value_column` (using the schema-canonical name when
available, falling back to the user's spelling) so update
set / where positions surface the column name.
**Shared sub-grammars** (`src/dsl/grammar/shared.rs`):
- New `slot_for_column(ty, name)` builds a `TypedValueSlot`
with the embedded leaked column name. Used by
`column_value_list`.
- New `slot_inner_for_type(ty)` returns just the Choice
(without TypedValueSlot wrapper) for slot_for_column to
rebuild.
- `column_value_list` factory now constructs per-column slots
via `slot_for_column(col.user_type, &col.name)`. Each slot
leaks its column name string with the same per-walk Box::leak
pattern the rest of dynamic dispatch uses.
**`WalkContext::pending_value_column: Option<String>`** (new
field, `src/dsl/walker/context.rs`). Pairs with
`pending_value_type` to give the hint resolver both pieces.
**Single-walk hint resolver** (`src/dsl/walker/mod.rs`):
- New `HintResolution { mode: HintMode, column: Option<String> }`
struct.
- New `hint_resolution_at_input(source, schema) -> Option<
HintResolution>` runs one walk and reports both pieces. The
ambient_hint dispatch composes per-column prose from the
result.
- Existing `hint_mode_at_input` / `hint_mode_at_input_with_schema`
preserved as thinner wrappers for tests / future callers
that don't need the column name.
**Catalog wrapper** (`src/friendly/strings/en-US.yaml`,
`src/friendly/keys.rs`):
- New `hint.value_slot_for_column: "for `{column}`: {detail}"`
prefixes the per-type prose with the actual column name when
the walker has it bound. Schemaless fallback continues to use
the generic value-literal prose with no column prefix.
**ambient_hint composes** (`src/input_render.rs`): consults
`hint_resolution_at_input`; when `column` is `Some`, wraps the
type prose through `hint.value_slot_for_column`; otherwise
emits the bare type prose.
Tests (846 total, 0 failing):
- 4 new input_render tests assert column names appear in the
prose at insert/update/where positions plus the
second-insert-value position (proves column tracking advances
with comma).
- All existing tests pass unchanged — the column-name addition
is layered on top of the type-only prose path.
Clippy clean.
This commit is contained in:
@@ -287,12 +287,17 @@ pub enum Node {
|
||||
/// column type in `WalkContext::pending_value_type` so the
|
||||
/// hint resolver can emit per-type catalog prose ("Type an
|
||||
/// integer", "Type a date as 'YYYY-MM-DD'", …) at empty
|
||||
/// prefix at this slot. The recorded type clears on a
|
||||
/// successful inner match — so positions BETWEEN typed
|
||||
/// slots (`insert into T values (1` mid-input) don't carry
|
||||
/// a stale hint type.
|
||||
/// prefix at this slot. When `column_name` is `Some`, the
|
||||
/// walker also writes `pending_value_column` so the hint
|
||||
/// can be rendered with the actual column name (e.g. "for
|
||||
/// `Email`: Type a quoted string …") rather than a generic
|
||||
/// type hint. The recorded values clear on a successful
|
||||
/// inner match — so positions BETWEEN typed slots
|
||||
/// (`insert into T values (1` mid-input) don't carry stale
|
||||
/// hint state.
|
||||
TypedValueSlot {
|
||||
ty: crate::dsl::types::Type,
|
||||
column_name: Option<&'static str>,
|
||||
inner: &'static Self,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -189,10 +189,12 @@ const INT_SLOT_CHOICES: &[Node] = &[
|
||||
const INT_SLOT_INNER: Node = Node::Choice(INT_SLOT_CHOICES);
|
||||
const INT_SLOT: Node = Node::TypedValueSlot {
|
||||
ty: Type::Int,
|
||||
column_name: None,
|
||||
inner: &INT_SLOT_INNER,
|
||||
};
|
||||
const SERIAL_SLOT: Node = Node::TypedValueSlot {
|
||||
ty: Type::Serial,
|
||||
column_name: None,
|
||||
inner: &INT_SLOT_INNER,
|
||||
};
|
||||
|
||||
@@ -200,6 +202,7 @@ const REAL_SLOT_CHOICES: &[Node] = &[Node::NumberLit { validator: None }, NULL_W
|
||||
const REAL_SLOT_INNER: Node = Node::Choice(REAL_SLOT_CHOICES);
|
||||
const REAL_SLOT: Node = Node::TypedValueSlot {
|
||||
ty: Type::Real,
|
||||
column_name: None,
|
||||
inner: &REAL_SLOT_INNER,
|
||||
};
|
||||
|
||||
@@ -212,6 +215,7 @@ const DECIMAL_SLOT_CHOICES: &[Node] = &[
|
||||
const DECIMAL_SLOT_INNER: Node = Node::Choice(DECIMAL_SLOT_CHOICES);
|
||||
const DECIMAL_SLOT: Node = Node::TypedValueSlot {
|
||||
ty: Type::Decimal,
|
||||
column_name: None,
|
||||
inner: &DECIMAL_SLOT_INNER,
|
||||
};
|
||||
|
||||
@@ -223,6 +227,7 @@ const BOOL_SLOT_CHOICES: &[Node] = &[
|
||||
const BOOL_SLOT_INNER: Node = Node::Choice(BOOL_SLOT_CHOICES);
|
||||
const BOOL_SLOT: Node = Node::TypedValueSlot {
|
||||
ty: Type::Bool,
|
||||
column_name: None,
|
||||
inner: &BOOL_SLOT_INNER,
|
||||
};
|
||||
|
||||
@@ -230,6 +235,7 @@ const TEXT_SLOT_CHOICES: &[Node] = &[Node::StringLit, NULL_WORD];
|
||||
const TEXT_SLOT_INNER: Node = Node::Choice(TEXT_SLOT_CHOICES);
|
||||
const TEXT_SLOT: Node = Node::TypedValueSlot {
|
||||
ty: Type::Text,
|
||||
column_name: None,
|
||||
inner: &TEXT_SLOT_INNER,
|
||||
};
|
||||
|
||||
@@ -238,30 +244,36 @@ const TEXT_SLOT: Node = Node::TypedValueSlot {
|
||||
// the YYYY-MM-DD / YYYY-MM-DD HH:MM:SS format examples.
|
||||
const DATE_SLOT: Node = Node::TypedValueSlot {
|
||||
ty: Type::Date,
|
||||
column_name: None,
|
||||
inner: &TEXT_SLOT_INNER,
|
||||
};
|
||||
const DATETIME_SLOT: Node = Node::TypedValueSlot {
|
||||
ty: Type::DateTime,
|
||||
column_name: None,
|
||||
inner: &TEXT_SLOT_INNER,
|
||||
};
|
||||
const BLOB_SLOT: Node = Node::TypedValueSlot {
|
||||
ty: Type::Blob,
|
||||
column_name: None,
|
||||
inner: &TEXT_SLOT_INNER,
|
||||
};
|
||||
// shortid columns store base58 text (ADR-0011 fk_target_type
|
||||
// shortid → text); the slot accepts a quoted-text literal or
|
||||
// null. The pre-Phase-D plain-Choice scaffolding had this
|
||||
// mapped to INT_SLOT (a holdover from the chumsky-side generic
|
||||
// VALUE_LITERAL fallback). Per-type dispatch corrects that.
|
||||
// null.
|
||||
const SHORTID_SLOT: Node = Node::TypedValueSlot {
|
||||
ty: Type::ShortId,
|
||||
column_name: None,
|
||||
inner: &TEXT_SLOT_INNER,
|
||||
};
|
||||
|
||||
/// Dispatch a value slot per user-facing type
|
||||
/// (ADR-0024 §slot_for_type). Returns the same node every time
|
||||
/// for a given Type — fine to call from within a
|
||||
/// `DynamicSubgrammar` factory.
|
||||
/// (ADR-0024 §slot_for_type).
|
||||
///
|
||||
/// Returns the same node every time for a given Type — fine to
|
||||
/// call from within a `DynamicSubgrammar` factory. The
|
||||
/// returned slot does not carry a column name; callers that
|
||||
/// have one (e.g. `column_value_list` building per-column
|
||||
/// slots) should use [`slot_for_column`] instead.
|
||||
#[must_use]
|
||||
pub const fn slot_for_type(ty: Type) -> Node {
|
||||
match ty {
|
||||
@@ -278,6 +290,45 @@ pub const fn slot_for_type(ty: Type) -> Node {
|
||||
}
|
||||
}
|
||||
|
||||
/// Look up just the inner Choice (no `TypedValueSlot` wrapper)
|
||||
/// for a given user-facing type. Used by `slot_for_column` to
|
||||
/// rebuild a TypedValueSlot with an embedded column name.
|
||||
const fn slot_inner_for_type(ty: Type) -> &'static Node {
|
||||
match ty {
|
||||
Type::Int | Type::Serial => &INT_SLOT_INNER,
|
||||
Type::Real => &REAL_SLOT_INNER,
|
||||
Type::Decimal => &DECIMAL_SLOT_INNER,
|
||||
Type::Bool => &BOOL_SLOT_INNER,
|
||||
Type::Text | Type::Date | Type::DateTime | Type::Blob | Type::ShortId => {
|
||||
&TEXT_SLOT_INNER
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a typed value slot with an embedded column name
|
||||
/// (ADR-0024 §Phase D §typed-value-slots). Used by
|
||||
/// `column_value_list` to attach each column's name so the
|
||||
/// hint resolver can render "for `<column>`:" prefixes.
|
||||
///
|
||||
/// The walker writes the (leaked) name into
|
||||
/// `WalkContext::pending_value_column` on entry to the slot
|
||||
/// and clears it on successful inner match.
|
||||
#[must_use]
|
||||
fn slot_for_column(ty: Type, name: &str) -> Node {
|
||||
// `Box::leak`: column names from the schema cache need a
|
||||
// `&'static str`-compatible lifetime to plug into the
|
||||
// static Node enum. The leak is per dynamic walk (factory
|
||||
// invocation), bounded by the column count — consistent
|
||||
// with the `DynamicSubgrammar` Box::leak in the walker
|
||||
// driver.
|
||||
let leaked: &'static str = Box::leak(name.to_string().into_boxed_str());
|
||||
Node::TypedValueSlot {
|
||||
ty,
|
||||
column_name: Some(leaked),
|
||||
inner: slot_inner_for_type(ty),
|
||||
}
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// Dynamic sub-grammar: column_value_list
|
||||
// =================================================================
|
||||
@@ -329,12 +380,15 @@ pub fn column_value_list(ctx: &WalkContext) -> Node {
|
||||
return FALLBACK_VALUE_LIST;
|
||||
}
|
||||
// Build a Seq of typed slots interleaved with commas.
|
||||
// Each slot embeds its column name so the hint resolver
|
||||
// can mention the column by name ("for `Email`: Type a
|
||||
// quoted string …").
|
||||
let mut children: Vec<Node> = Vec::with_capacity(cols.len() * 2);
|
||||
for (i, col) in cols.iter().enumerate() {
|
||||
if i > 0 {
|
||||
children.push(Node::Punct(','));
|
||||
}
|
||||
children.push(slot_for_type(col.user_type));
|
||||
children.push(slot_for_column(col.user_type, &col.name));
|
||||
}
|
||||
Node::Seq(Box::leak(children.into_boxed_slice()))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user