feat(hint): H2 Phase B — per-form keying + the three exemplars (ADR-0053)
The first exemplar (`add 1:n relationship`) showed per-node keying is too coarse for multi-form commands, so revise the mechanism to per-form. - CommandNode `hint_id: Option<&str>` -> `hint_ids: &[&str]` (mirrors usage_ids); hint_key_for_input_in_mode reuses a factored-out pick_form_key (shared digit/m:n/suffix form disambiguation with usage_key_for_input_in_mode) - wire INSERT + ADD (all four forms) with hint_ids - author the three approved exemplars: hint.cmd.insert, hint.cmd.add_relationship, hint.err.foreign_key.child_side (what/example/concept) + keys.rs registration - revise ADR-0053 D3 to per-form; record clause-concept hints as a deferred extension (issue #37); update README + plan - +5 tests; 2488 pass / 1 ignored, clippy clean
This commit is contained in:
+50
-1
@@ -3152,7 +3152,7 @@ impl App {
|
||||
let (view, cursor, _off) = self.feedback_view();
|
||||
let probe = view.to_string();
|
||||
let mode = self.effective_mode().as_mode();
|
||||
if let Some(id) = crate::dsl::grammar::hint_id_for_input_in_mode(&probe, mode)
|
||||
if let Some(id) = crate::dsl::grammar::hint_key_for_input_in_mode(&probe, mode)
|
||||
&& self.emit_tier3_block(&format!("hint.cmd.{id}"))
|
||||
{
|
||||
return;
|
||||
@@ -5800,6 +5800,55 @@ mod tests {
|
||||
assert!(output_contains(&app, "explain the most recent error"));
|
||||
}
|
||||
|
||||
// ── Phase B: tier-3 exemplar content renders ────────────────
|
||||
|
||||
#[test]
|
||||
fn f1_on_insert_input_renders_the_insert_hint_block() {
|
||||
let mut app = App::new();
|
||||
type_str(&mut app, "insert into Customers ");
|
||||
f1(&mut app);
|
||||
assert!(
|
||||
output_contains(&app, "Add one or more rows to a table"),
|
||||
"expected the insert tier-3 block"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn f1_on_add_relationship_renders_the_relationship_block() {
|
||||
let mut app = App::new();
|
||||
type_str(&mut app, "add 1:n relationship from Customers.id to Orders.cust ");
|
||||
f1(&mut app);
|
||||
assert!(
|
||||
output_contains(&app, "one parent, many children"),
|
||||
"expected the add-relationship tier-3 block"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn f1_on_add_column_does_not_render_the_relationship_block() {
|
||||
// Per-form disambiguation (ADR-0053 D3): `add column` resolves
|
||||
// to `add_column` (no tier-3 block yet → tier-2 fallback), NOT
|
||||
// the relationship block — proving the multi-form node keys
|
||||
// per form, not per node.
|
||||
let mut app = App::new();
|
||||
type_str(&mut app, "add column Note text to Customers");
|
||||
f1(&mut app);
|
||||
assert!(!output_contains(&app, "one parent, many children"));
|
||||
assert!(!output_contains(&app, "1:n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hint_renders_the_foreign_key_error_block() {
|
||||
let mut app = App::new();
|
||||
app.last_error_hint_key = Some("foreign_key.child_side".to_string());
|
||||
type_str(&mut app, "hint");
|
||||
submit(&mut app);
|
||||
assert!(
|
||||
output_contains(&app, "doesn't match any parent row"),
|
||||
"expected the FK child-side tier-3 block"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn messages_command_toggles_verbosity_and_reports() {
|
||||
let mut app = App::new();
|
||||
|
||||
+14
-14
@@ -266,7 +266,7 @@ pub static QUIT: CommandNode = CommandNode {
|
||||
shape: EMPTY_SEQ,
|
||||
ast_builder: build_quit,
|
||||
help_id: Some("app.quit"),
|
||||
hint_id: None,
|
||||
hint_ids: &[],
|
||||
usage_ids: &["parse.usage.quit"],};
|
||||
|
||||
pub static HELP: CommandNode = CommandNode {
|
||||
@@ -274,7 +274,7 @@ pub static HELP: CommandNode = CommandNode {
|
||||
shape: HELP_TOPIC_OPT,
|
||||
ast_builder: build_help,
|
||||
help_id: Some("app.help"),
|
||||
hint_id: None,
|
||||
hint_ids: &[],
|
||||
usage_ids: &["parse.usage.help"],};
|
||||
|
||||
pub static HINT: CommandNode = CommandNode {
|
||||
@@ -283,7 +283,7 @@ pub static HINT: CommandNode = CommandNode {
|
||||
ast_builder: build_hint,
|
||||
help_id: Some("app.hint"),
|
||||
// hint_id assigned in Phase C with the tier-3 corpus (ADR-0053).
|
||||
hint_id: None,
|
||||
hint_ids: &[],
|
||||
usage_ids: &["parse.usage.hint"],};
|
||||
|
||||
pub static REBUILD: CommandNode = CommandNode {
|
||||
@@ -291,7 +291,7 @@ pub static REBUILD: CommandNode = CommandNode {
|
||||
shape: EMPTY_SEQ,
|
||||
ast_builder: build_rebuild,
|
||||
help_id: Some("app.rebuild"),
|
||||
hint_id: None,
|
||||
hint_ids: &[],
|
||||
usage_ids: &["parse.usage.rebuild"],};
|
||||
|
||||
pub static SAVE: CommandNode = CommandNode {
|
||||
@@ -299,7 +299,7 @@ pub static SAVE: CommandNode = CommandNode {
|
||||
shape: SAVE_AS_OPT,
|
||||
ast_builder: build_save,
|
||||
help_id: Some("app.save"),
|
||||
hint_id: None,
|
||||
hint_ids: &[],
|
||||
usage_ids: &["parse.usage.save"],};
|
||||
|
||||
pub static NEW: CommandNode = CommandNode {
|
||||
@@ -307,7 +307,7 @@ pub static NEW: CommandNode = CommandNode {
|
||||
shape: EMPTY_SEQ,
|
||||
ast_builder: build_new,
|
||||
help_id: Some("app.new"),
|
||||
hint_id: None,
|
||||
hint_ids: &[],
|
||||
usage_ids: &["parse.usage.new"],};
|
||||
|
||||
pub static LOAD: CommandNode = CommandNode {
|
||||
@@ -315,7 +315,7 @@ pub static LOAD: CommandNode = CommandNode {
|
||||
shape: EMPTY_SEQ,
|
||||
ast_builder: build_load,
|
||||
help_id: Some("app.load"),
|
||||
hint_id: None,
|
||||
hint_ids: &[],
|
||||
usage_ids: &["parse.usage.load"],};
|
||||
|
||||
pub static EXPORT: CommandNode = CommandNode {
|
||||
@@ -323,7 +323,7 @@ pub static EXPORT: CommandNode = CommandNode {
|
||||
shape: EXPORT_PATH_OPT,
|
||||
ast_builder: build_export,
|
||||
help_id: Some("app.export"),
|
||||
hint_id: None,
|
||||
hint_ids: &[],
|
||||
usage_ids: &["parse.usage.export"],};
|
||||
|
||||
pub static IMPORT: CommandNode = CommandNode {
|
||||
@@ -331,7 +331,7 @@ pub static IMPORT: CommandNode = CommandNode {
|
||||
shape: IMPORT_BODY_OPT,
|
||||
ast_builder: build_import,
|
||||
help_id: Some("app.import"),
|
||||
hint_id: None,
|
||||
hint_ids: &[],
|
||||
usage_ids: &["parse.usage.import"],};
|
||||
|
||||
pub static MODE: CommandNode = CommandNode {
|
||||
@@ -339,7 +339,7 @@ pub static MODE: CommandNode = CommandNode {
|
||||
shape: MODE_VALUE,
|
||||
ast_builder: build_mode,
|
||||
help_id: Some("app.mode"),
|
||||
hint_id: None,
|
||||
hint_ids: &[],
|
||||
usage_ids: &["parse.usage.mode"],};
|
||||
|
||||
pub static MESSAGES: CommandNode = CommandNode {
|
||||
@@ -347,7 +347,7 @@ pub static MESSAGES: CommandNode = CommandNode {
|
||||
shape: MESSAGES_VALUE_OPT,
|
||||
ast_builder: build_messages,
|
||||
help_id: Some("app.messages"),
|
||||
hint_id: None,
|
||||
hint_ids: &[],
|
||||
usage_ids: &["parse.usage.messages"],};
|
||||
|
||||
pub static UNDO: CommandNode = CommandNode {
|
||||
@@ -355,7 +355,7 @@ pub static UNDO: CommandNode = CommandNode {
|
||||
shape: EMPTY_SEQ,
|
||||
ast_builder: build_undo,
|
||||
help_id: Some("app.undo"),
|
||||
hint_id: None,
|
||||
hint_ids: &[],
|
||||
usage_ids: &["parse.usage.undo"],};
|
||||
|
||||
pub static REDO: CommandNode = CommandNode {
|
||||
@@ -363,7 +363,7 @@ pub static REDO: CommandNode = CommandNode {
|
||||
shape: EMPTY_SEQ,
|
||||
ast_builder: build_redo,
|
||||
help_id: Some("app.redo"),
|
||||
hint_id: None,
|
||||
hint_ids: &[],
|
||||
usage_ids: &["parse.usage.redo"],};
|
||||
|
||||
pub static COPY: CommandNode = CommandNode {
|
||||
@@ -371,5 +371,5 @@ pub static COPY: CommandNode = CommandNode {
|
||||
shape: COPY_VALUE_OPT,
|
||||
ast_builder: build_copy,
|
||||
help_id: Some("app.copy"),
|
||||
hint_id: None,
|
||||
hint_ids: &[],
|
||||
usage_ids: &["parse.usage.copy"],};
|
||||
|
||||
+14
-13
@@ -1790,7 +1790,7 @@ pub static SHOW: CommandNode = CommandNode {
|
||||
shape: SHOW_SHAPE,
|
||||
ast_builder: build_show,
|
||||
help_id: Some("data.show"),
|
||||
hint_id: None,
|
||||
hint_ids: &[],
|
||||
usage_ids: &[
|
||||
"parse.usage.show_data",
|
||||
"parse.usage.show_table",
|
||||
@@ -1806,7 +1806,7 @@ pub static SEED: CommandNode = CommandNode {
|
||||
shape: SEED_SHAPE,
|
||||
ast_builder: build_seed,
|
||||
help_id: Some("data.seed"),
|
||||
hint_id: None,
|
||||
hint_ids: &[],
|
||||
usage_ids: &["parse.usage.seed"],
|
||||
};
|
||||
|
||||
@@ -1815,7 +1815,8 @@ pub static INSERT: CommandNode = CommandNode {
|
||||
shape: INSERT_SHAPE,
|
||||
ast_builder: build_insert,
|
||||
help_id: Some("data.insert"),
|
||||
hint_id: None,
|
||||
// ADR-0053 Phase-B exemplar.
|
||||
hint_ids: &["insert"],
|
||||
usage_ids: &["parse.usage.insert"],};
|
||||
|
||||
pub static UPDATE: CommandNode = CommandNode {
|
||||
@@ -1823,7 +1824,7 @@ pub static UPDATE: CommandNode = CommandNode {
|
||||
shape: UPDATE_SHAPE,
|
||||
ast_builder: build_update,
|
||||
help_id: Some("data.update"),
|
||||
hint_id: None,
|
||||
hint_ids: &[],
|
||||
usage_ids: &["parse.usage.update"],};
|
||||
|
||||
pub static DELETE: CommandNode = CommandNode {
|
||||
@@ -1831,7 +1832,7 @@ pub static DELETE: CommandNode = CommandNode {
|
||||
shape: DELETE_SHAPE,
|
||||
ast_builder: build_delete,
|
||||
help_id: Some("data.delete"),
|
||||
hint_id: None,
|
||||
hint_ids: &[],
|
||||
usage_ids: &["parse.usage.delete"],};
|
||||
|
||||
pub static REPLAY: CommandNode = CommandNode {
|
||||
@@ -1839,7 +1840,7 @@ pub static REPLAY: CommandNode = CommandNode {
|
||||
shape: REPLAY_PATH,
|
||||
ast_builder: build_replay,
|
||||
help_id: Some("data.replay"),
|
||||
hint_id: None,
|
||||
hint_ids: &[],
|
||||
usage_ids: &["parse.usage.replay"],};
|
||||
|
||||
pub static EXPLAIN: CommandNode = CommandNode {
|
||||
@@ -1847,7 +1848,7 @@ pub static EXPLAIN: CommandNode = CommandNode {
|
||||
shape: EXPLAIN_SHAPE,
|
||||
ast_builder: build_explain,
|
||||
help_id: Some("data.explain"),
|
||||
hint_id: None,
|
||||
hint_ids: &[],
|
||||
usage_ids: &["parse.usage.explain"],};
|
||||
|
||||
/// `explain` over advanced-mode SQL (ADR-0039).
|
||||
@@ -1867,7 +1868,7 @@ pub static EXPLAIN_SQL: CommandNode = CommandNode {
|
||||
// too). Mirrors the `SQL_INSERT`/`SQL_UPDATE`/`SQL_DELETE`
|
||||
// precedent; otherwise `note_help` would print `explain` twice.
|
||||
help_id: None,
|
||||
hint_id: None,
|
||||
hint_ids: &[],
|
||||
usage_ids: &[],};
|
||||
|
||||
/// SQL `SELECT` (ADR-0030 §6, ADR-0031, ADR-0032).
|
||||
@@ -1883,7 +1884,7 @@ pub static SELECT: CommandNode = CommandNode {
|
||||
shape: Node::Subgrammar(&sql_select::SQL_SELECT_TAIL),
|
||||
ast_builder: build_select,
|
||||
help_id: None,
|
||||
hint_id: None,
|
||||
hint_ids: &[],
|
||||
usage_ids: &["parse.usage.select"],};
|
||||
|
||||
/// `WITH …` top-level statement (ADR-0032 §4 / sub-phase 2c).
|
||||
@@ -1898,7 +1899,7 @@ pub static WITH: CommandNode = CommandNode {
|
||||
shape: Node::Subgrammar(&sql_select::SQL_WITH_TAIL),
|
||||
ast_builder: build_select,
|
||||
help_id: None,
|
||||
hint_id: None,
|
||||
hint_ids: &[],
|
||||
usage_ids: &["parse.usage.with"],};
|
||||
|
||||
/// SQL `INSERT` — the `Advanced`-category node of the shared
|
||||
@@ -1916,7 +1917,7 @@ pub static SQL_INSERT: CommandNode = CommandNode {
|
||||
shape: Node::Subgrammar(&sql_insert::SQL_INSERT_SHAPE),
|
||||
ast_builder: build_sql_insert,
|
||||
help_id: None,
|
||||
hint_id: None,
|
||||
hint_ids: &[],
|
||||
usage_ids: &[],
|
||||
};
|
||||
|
||||
@@ -1930,7 +1931,7 @@ pub static SQL_UPDATE: CommandNode = CommandNode {
|
||||
shape: Node::Subgrammar(&sql_update::SQL_UPDATE_SHAPE),
|
||||
ast_builder: build_sql_update,
|
||||
help_id: None,
|
||||
hint_id: None,
|
||||
hint_ids: &[],
|
||||
usage_ids: &[],
|
||||
};
|
||||
|
||||
@@ -1946,7 +1947,7 @@ pub static SQL_DELETE: CommandNode = CommandNode {
|
||||
shape: Node::Subgrammar(&sql_delete::SQL_DELETE_SHAPE),
|
||||
ast_builder: build_sql_delete,
|
||||
help_id: None,
|
||||
hint_id: None,
|
||||
hint_ids: &[],
|
||||
usage_ids: &[],
|
||||
};
|
||||
|
||||
|
||||
+20
-11
@@ -968,7 +968,7 @@ pub static DROP: CommandNode = CommandNode {
|
||||
shape: DROP_SHAPE,
|
||||
ast_builder: build_drop,
|
||||
help_id: Some("ddl.drop"),
|
||||
hint_id: None,
|
||||
hint_ids: &[],
|
||||
usage_ids: &[
|
||||
"parse.usage.drop_table",
|
||||
"parse.usage.drop_column",
|
||||
@@ -982,7 +982,16 @@ pub static ADD: CommandNode = CommandNode {
|
||||
shape: ADD_SHAPE,
|
||||
ast_builder: build_add,
|
||||
help_id: Some("ddl.add"),
|
||||
hint_id: None,
|
||||
// Per-form (ADR-0053 D3): every form is listed so the form-word
|
||||
// disambiguation resolves correctly; forms without an authored
|
||||
// block yet fall back to tier-2 at render. `add_relationship` is
|
||||
// authored as a Phase-B exemplar.
|
||||
hint_ids: &[
|
||||
"add_column",
|
||||
"add_relationship",
|
||||
"add_index",
|
||||
"add_constraint",
|
||||
],
|
||||
usage_ids: &[
|
||||
"parse.usage.add_column",
|
||||
"parse.usage.add_relationship",
|
||||
@@ -995,7 +1004,7 @@ pub static RENAME: CommandNode = CommandNode {
|
||||
shape: RENAME_COLUMN,
|
||||
ast_builder: build_rename_column,
|
||||
help_id: Some("ddl.rename"),
|
||||
hint_id: None,
|
||||
hint_ids: &[],
|
||||
usage_ids: &["parse.usage.rename_column"],};
|
||||
|
||||
pub static CHANGE: CommandNode = CommandNode {
|
||||
@@ -1003,7 +1012,7 @@ pub static CHANGE: CommandNode = CommandNode {
|
||||
shape: CHANGE_COLUMN,
|
||||
ast_builder: build_change_column,
|
||||
help_id: Some("ddl.change"),
|
||||
hint_id: None,
|
||||
hint_ids: &[],
|
||||
usage_ids: &["parse.usage.change_column"],};
|
||||
|
||||
// =================================================================
|
||||
@@ -1364,7 +1373,7 @@ pub static CREATE: CommandNode = CommandNode {
|
||||
shape: CREATE_TABLE,
|
||||
ast_builder: build_create_table,
|
||||
help_id: Some("ddl.create"),
|
||||
hint_id: None,
|
||||
hint_ids: &[],
|
||||
usage_ids: &["parse.usage.create_table"],};
|
||||
|
||||
// =================================================================
|
||||
@@ -1433,7 +1442,7 @@ pub static CREATE_M2N: CommandNode = CommandNode {
|
||||
shape: CREATE_M2N_SHAPE,
|
||||
ast_builder: build_create_m2n,
|
||||
help_id: Some("ddl.create_m2n"),
|
||||
hint_id: None,
|
||||
hint_ids: &[],
|
||||
usage_ids: &["parse.usage.create_m2n"],
|
||||
};
|
||||
|
||||
@@ -1864,7 +1873,7 @@ pub static SQL_CREATE_TABLE: CommandNode = CommandNode {
|
||||
shape: Node::Subgrammar(&super::sql_create_table::SQL_CREATE_TABLE_SHAPE),
|
||||
ast_builder: build_sql_create_table,
|
||||
help_id: Some("ddl.sql_create_table"),
|
||||
hint_id: None,
|
||||
hint_ids: &[],
|
||||
usage_ids: &["parse.usage.sql_create_table"],
|
||||
};
|
||||
|
||||
@@ -1884,7 +1893,7 @@ pub static SQL_DROP_TABLE: CommandNode = CommandNode {
|
||||
shape: SQL_DROP_TABLE_SHAPE,
|
||||
ast_builder: build_sql_drop_table,
|
||||
help_id: Some("ddl.sql_drop_table"),
|
||||
hint_id: None,
|
||||
hint_ids: &[],
|
||||
usage_ids: &["parse.usage.sql_drop_table"],
|
||||
};
|
||||
|
||||
@@ -1904,7 +1913,7 @@ pub static SQL_DROP_INDEX: CommandNode = CommandNode {
|
||||
shape: SQL_DROP_INDEX_SHAPE,
|
||||
ast_builder: build_sql_drop_index,
|
||||
help_id: Some("ddl.sql_drop_index"),
|
||||
hint_id: None,
|
||||
hint_ids: &[],
|
||||
usage_ids: &["parse.usage.sql_drop_index"],
|
||||
};
|
||||
|
||||
@@ -1986,7 +1995,7 @@ pub static SQL_CREATE_INDEX: CommandNode = CommandNode {
|
||||
shape: SQL_CREATE_INDEX_SHAPE,
|
||||
ast_builder: build_sql_create_index,
|
||||
help_id: Some("ddl.sql_create_index"),
|
||||
hint_id: None,
|
||||
hint_ids: &[],
|
||||
usage_ids: &["parse.usage.sql_create_index"],
|
||||
};
|
||||
|
||||
@@ -2545,7 +2554,7 @@ pub static SQL_ALTER_TABLE: CommandNode = CommandNode {
|
||||
shape: SQL_ALTER_TABLE_SHAPE,
|
||||
ast_builder: build_sql_alter_table,
|
||||
help_id: Some("ddl.sql_alter_table"),
|
||||
hint_id: None,
|
||||
hint_ids: &[],
|
||||
usage_ids: &["parse.usage.sql_alter_table"],
|
||||
};
|
||||
|
||||
|
||||
+80
-29
@@ -530,15 +530,18 @@ pub struct CommandNode {
|
||||
/// so a newly-registered command appears in `help`
|
||||
/// automatically (ADR-0024 §help_id).
|
||||
pub help_id: Option<&'static str>,
|
||||
/// Catalog key stem (`hint.cmd.<id>`) for this command form's
|
||||
/// **tier-3** contextual hint (ADR-0053 / H2). Unlike `help_id`
|
||||
/// — which is `None` on advanced-SQL forms purely to dedup the
|
||||
/// `help` list — `hint_id` is 1:1 with command *forms*, so each
|
||||
/// advanced-SQL form carries its own id and renders SQL-syntax
|
||||
/// content distinct from its simple-DSL sibling. `None` until a
|
||||
/// form's tier-3 block is authored (the surface falls back to
|
||||
/// tier-2 ambient/error text).
|
||||
pub hint_id: Option<&'static str>,
|
||||
/// Catalog key stems (`hint.cmd.<id>`) for this command's
|
||||
/// **tier-3** contextual hints (ADR-0053 / H2), **one per form**,
|
||||
/// mirroring `usage_ids`. A single-form command carries one; a
|
||||
/// multi-form command (`add`, `drop`, `show`, `create`) carries
|
||||
/// one per form so a live-input hint can be specific to the form
|
||||
/// being typed (`hint.cmd.add_relationship`, not a shared `add`
|
||||
/// block). `hint_key_for_input_in_mode` disambiguates by the form
|
||||
/// word, reusing `usage_key_for_input_in_mode`'s logic. Empty
|
||||
/// until a form's tier-3 block is authored (the surface falls back
|
||||
/// to tier-2 ambient/error text). Distinct from `help_id` (which is
|
||||
/// `None` on advanced-SQL forms purely to dedup the `help` list).
|
||||
pub hint_ids: &'static [&'static str],
|
||||
/// Catalog keys under `parse.usage.*` to render in the
|
||||
/// "usage:" block when a parse error fires for this command
|
||||
/// (ADR-0021 §1, ADR-0024 §architecture). Multi-form families
|
||||
@@ -602,20 +605,30 @@ pub fn usage_keys_for_input_in_mode(
|
||||
Some((entry, keys))
|
||||
}
|
||||
|
||||
/// The tier-3 `hint_id` of the command form `source` is currently
|
||||
/// typing, in `mode` (H2 / ADR-0053).
|
||||
/// The single tier-3 hint key (`hint.cmd.<id>` stem) for the command
|
||||
/// **form** `source` is currently typing, in `mode` (H2 / ADR-0053).
|
||||
///
|
||||
/// Reuses the same mode-aware
|
||||
/// selection as [`usage_keys_for_input_in_mode`] and returns the
|
||||
/// **mode-primary** node's `hint_id` — so an advanced-SQL form
|
||||
/// resolves to its *own* id, not its simple-DSL sibling's. `None` if
|
||||
/// no entry word matches, or the chosen form has no tier-3 block yet
|
||||
/// (the caller then falls back to tier-2 ambient text).
|
||||
/// Mirrors [`usage_key_for_input_in_mode`]: the union of the
|
||||
/// mode-selected nodes' `hint_ids`, disambiguated to the typed form by
|
||||
/// [`pick_form_key`] — so `add 1:n relationship` resolves to the
|
||||
/// relationship hint, and an advanced-SQL form resolves to its own
|
||||
/// (not its simple sibling's). `None` if no entry word matches or the
|
||||
/// form has no tier-3 block yet (the caller falls back to tier-2).
|
||||
#[must_use]
|
||||
pub fn hint_id_for_input_in_mode(source: &str, mode: crate::mode::Mode) -> Option<&'static str> {
|
||||
selected_nodes_for_input_in_mode(source, mode)
|
||||
.first()
|
||||
.and_then(|(_, node, _)| node.hint_id)
|
||||
pub fn hint_key_for_input_in_mode(source: &str, mode: crate::mode::Mode) -> Option<&'static str> {
|
||||
let nodes = selected_nodes_for_input_in_mode(source, mode);
|
||||
if nodes.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut keys: Vec<&'static str> = Vec::new();
|
||||
for (_, node, _) in &nodes {
|
||||
for k in node.hint_ids {
|
||||
if !keys.contains(k) {
|
||||
keys.push(*k);
|
||||
}
|
||||
}
|
||||
}
|
||||
pick_form_key(source, &keys)
|
||||
}
|
||||
|
||||
/// Shared mode-aware command-form selection for the entry word at the
|
||||
@@ -694,14 +707,24 @@ pub fn usage_key_for_input_in_mode(
|
||||
source: &str,
|
||||
mode: crate::mode::Mode,
|
||||
) -> Option<&'static str> {
|
||||
use crate::dsl::walker::lex_helpers::{consume_ident, skip_whitespace};
|
||||
let (_entry, keys) = usage_keys_for_input_in_mode(source, mode)?;
|
||||
pick_form_key(source, &keys)
|
||||
}
|
||||
|
||||
/// From the form word after the entry keyword, pick the single `keys`
|
||||
/// entry for the form `source` names.
|
||||
///
|
||||
/// A single-entry list resolves to its one key; a multi-form list
|
||||
/// disambiguates by the form word (`add 1:n relationship` → the
|
||||
/// `…relationship` key, `create m:n …` → the `…m2n` key, else the
|
||||
/// identifier form word matched against each key's suffix). Shared by
|
||||
/// the usage-template and tier-3-hint single-key lookups so they agree.
|
||||
fn pick_form_key<'a>(source: &str, keys: &[&'a str]) -> Option<&'a str> {
|
||||
use crate::dsl::walker::lex_helpers::{consume_ident, skip_whitespace};
|
||||
let first = *keys.first()?;
|
||||
if keys.len() == 1 {
|
||||
return Some(first);
|
||||
}
|
||||
// Multi-form: the form is named by the token right after
|
||||
// the entry keyword.
|
||||
let start = skip_whitespace(source, 0);
|
||||
let (_, entry_end) = consume_ident(source, start)?;
|
||||
let after = skip_whitespace(source, entry_end);
|
||||
@@ -710,14 +733,12 @@ pub fn usage_key_for_input_in_mode(
|
||||
return keys.iter().copied().find(|k| k.ends_with("relationship"));
|
||||
}
|
||||
// The `create m:n relationship` form (ADR-0045) opens with `m:n`
|
||||
// — a letter, so the digit branch misses it, and its usage key ends
|
||||
// `…create_m2n` (not `relationship`).
|
||||
// — a letter, so the digit branch misses it; its key ends `…m2n`.
|
||||
if source[after..].get(..3).is_some_and(|s| s.eq_ignore_ascii_case("m:n")) {
|
||||
return keys.iter().copied().find(|k| k.ends_with("m2n"));
|
||||
}
|
||||
// Otherwise the form word is an identifier — `column`,
|
||||
// `index`, `table`, `relationship` — matched against the
|
||||
// usage key's suffix.
|
||||
// Otherwise the form word is an identifier — `column`, `index`,
|
||||
// `table`, `relationship` — matched against each key's suffix.
|
||||
let (s, e) = consume_ident(source, after)?;
|
||||
let form = source[s..e].to_ascii_lowercase();
|
||||
keys.iter().copied().find(|k| k.ends_with(form.as_str()))
|
||||
@@ -873,6 +894,36 @@ pub fn commands_for_entry_word(
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod hint_key_tests {
|
||||
use super::hint_key_for_input_in_mode;
|
||||
use crate::mode::Mode;
|
||||
|
||||
/// Per-form hint keying (ADR-0053 D3): a multi-form command
|
||||
/// resolves the *typed* form, not the node — `add 1:n
|
||||
/// relationship` → the relationship hint, `add column` → the
|
||||
/// (as-yet-unauthored) column hint, never the wrong form.
|
||||
#[test]
|
||||
fn hint_key_resolves_the_typed_form() {
|
||||
assert_eq!(
|
||||
hint_key_for_input_in_mode("add 1:n relationship from A.x to B.y", Mode::Simple),
|
||||
Some("add_relationship")
|
||||
);
|
||||
assert_eq!(
|
||||
hint_key_for_input_in_mode("add column Note text to T", Mode::Simple),
|
||||
Some("add_column")
|
||||
);
|
||||
assert_eq!(
|
||||
hint_key_for_input_in_mode("insert into T values (1)", Mode::Simple),
|
||||
Some("insert")
|
||||
);
|
||||
// A node with no hint_ids yet → None (tier-2 fallback).
|
||||
assert_eq!(hint_key_for_input_in_mode("drop table T", Mode::Simple), None);
|
||||
// Unknown entry word → None.
|
||||
assert_eq!(hint_key_for_input_in_mode("zzz", Mode::Simple), None);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod usage_key_tests {
|
||||
use super::usage_key_for_input;
|
||||
|
||||
@@ -6910,7 +6910,7 @@ mod dispatch_3a_tests {
|
||||
shape: Node::Word(Word::keyword("dsltail")),
|
||||
ast_builder: dsl_builder,
|
||||
help_id: None,
|
||||
hint_id: None,
|
||||
hint_ids: &[],
|
||||
usage_ids: &[],
|
||||
};
|
||||
static SMOKE_SQL: CommandNode = CommandNode {
|
||||
@@ -6918,7 +6918,7 @@ mod dispatch_3a_tests {
|
||||
shape: Node::Word(Word::keyword("sqltail")),
|
||||
ast_builder: sql_builder,
|
||||
help_id: None,
|
||||
hint_id: None,
|
||||
hint_ids: &[],
|
||||
usage_ids: &[],
|
||||
};
|
||||
|
||||
|
||||
@@ -224,6 +224,16 @@ pub const KEYS_AND_PLACEHOLDERS: &[(&str, &[&str])] = &[
|
||||
),
|
||||
("hint.ambient_expected", &["expected"]),
|
||||
("hint.getting_started", &[]),
|
||||
// Tier-3 teaching blocks (ADR-0053 D3) — Phase-B exemplars.
|
||||
("hint.cmd.insert.what", &[]),
|
||||
("hint.cmd.insert.example", &[]),
|
||||
("hint.cmd.insert.concept", &[]),
|
||||
("hint.cmd.add_relationship.what", &[]),
|
||||
("hint.cmd.add_relationship.example", &[]),
|
||||
("hint.cmd.add_relationship.concept", &[]),
|
||||
("hint.err.foreign_key.child_side.what", &[]),
|
||||
("hint.err.foreign_key.child_side.example", &[]),
|
||||
("hint.err.foreign_key.child_side.concept", &[]),
|
||||
(
|
||||
"hint.ambient_invalid_ident",
|
||||
&["kind", "found"],
|
||||
|
||||
@@ -391,6 +391,27 @@ hint:
|
||||
# H2 / ADR-0053: shown by `hint` / F1 when there is nothing specific
|
||||
# to expand on (no recent error, empty input).
|
||||
getting_started: "Start typing a command and press F1 for a hint, or type `help` for the full command list."
|
||||
# ── Tier-3 teaching blocks (ADR-0053 D3) ──────────────────────────
|
||||
# Per-form command hints (`hint.cmd.<form>`) and per-class error
|
||||
# hints (`hint.err.<class>`), each a `what` (1–2 sentences) / `example`
|
||||
# (one runnable, mode-correct line) / `concept` (the relational idea —
|
||||
# the teaching part). Phase B seeds the three approved exemplars; the
|
||||
# rest are authored in Phase C.
|
||||
cmd:
|
||||
insert:
|
||||
what: "Add one or more rows to a table."
|
||||
example: "insert into Customers values ('Ann', 'ann@example.io')"
|
||||
concept: "A row is one record; each value lines up with a column, in order. Columns typed `serial`/`shortid` fill themselves — leave them out."
|
||||
add_relationship:
|
||||
what: "Link two tables so a parent row can own many child rows."
|
||||
example: "add 1:n relationship from Customers.id to Orders.customer_id"
|
||||
concept: "The \"1:n\" means one parent, many children. The child column holds the foreign key; add `--create-fk` to create that column if it doesn't exist yet."
|
||||
err:
|
||||
foreign_key:
|
||||
child_side:
|
||||
what: "The value you gave for the child column doesn't match any parent row, so the foreign key has nothing to point at."
|
||||
example: "First insert the parent (insert into Customers …), then the child that references it."
|
||||
concept: "A foreign key is a promise that every child points at a real parent, so the parent must exist first. To allow orphans on delete instead, set the relationship's `on delete` to `set null` or `cascade`."
|
||||
# Invalid identifier in a schema slot (ADR-0022 stage 8e
|
||||
# + the user's #5). Voice mirrors ADR-0019's "no such
|
||||
# {kind}" wording for consistency with engine errors.
|
||||
|
||||
Reference in New Issue
Block a user