feat: DSL→SQL teaching echo — channel + create-table slice (ADR-0037 + ADR-0038)
Walking skeleton validating the whole echo architecture end to end; the Command→SQL renderer currently covers `create table`, with the rest of Bucket A / B / category-3 to follow (ADR-0038 §8). - Channel (ADR-0037): the three-way EffectiveMode (reusing the existing enum, not a new SubmissionMode — recorded in the ADR) rides on Action::ExecuteDsl to the runtime. `replay` bypasses the interactive spawn, so it never echoes (silent, for free). - Echo (ADR-0038): built at the runtime's ExecuteDsl dispatch — the worker gets decomposed calls, not the Command, so ADR §4's "worker builds it" was corrected to the dispatch layer. Gated by echo_for (advanced effective mode + DSL-form). Carried on DslSucceeded; rendered by note_ok_summary as `Executing SQL: …` immediately beneath `[ok]`. New src/echo.rs renderer; echo.executing_sql i18n key. - command_to_sql: `create table` → `CREATE TABLE T (id serial PRIMARY KEY)` (single inline / compound table-level PK), playground type vocabulary, round-trip-verified against the advanced walker (the §1 contract). Tests: echo.rs (render, round-trip contract, mode gate, Sql*-not-echoed); app.rs (submit carries the 3-way mode; echo renders beneath [ok]). Suite 1970/0/1; clippy clean.
This commit is contained in:
+102
-11
@@ -216,6 +216,12 @@ pub struct App {
|
||||
/// flag; the `undo` / `redo` commands then report undo is off
|
||||
/// rather than emitting a prepare action.
|
||||
pub undo_enabled: bool,
|
||||
/// The DSL → SQL teaching echo (ADR-0038) for the command currently
|
||||
/// being rendered: set from the success event just before its handler
|
||||
/// runs, consumed by `note_ok_summary` (which pushes it beneath
|
||||
/// `[ok]`), within the same synchronous `update()` call. `None` when
|
||||
/// the command has no echo.
|
||||
pending_echo: Option<String>,
|
||||
}
|
||||
|
||||
/// Dialogs that take over keyboard input when active.
|
||||
@@ -346,6 +352,7 @@ impl App {
|
||||
// Undo is on by default; the runtime flips this off for
|
||||
// a `--no-undo` session (ADR-0006 Amendment 1).
|
||||
undo_enabled: true,
|
||||
pending_echo: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -438,7 +445,11 @@ impl App {
|
||||
AppEvent::DslSucceeded {
|
||||
command,
|
||||
description,
|
||||
echo,
|
||||
} => {
|
||||
// Stash the teaching echo (ADR-0038) for `note_ok_summary`
|
||||
// to render beneath `[ok]` — consumed synchronously below.
|
||||
self.pending_echo = echo;
|
||||
self.handle_dsl_success(&command, description);
|
||||
Vec::new()
|
||||
}
|
||||
@@ -1113,12 +1124,16 @@ impl App {
|
||||
|
||||
// `:` one-shot escape: in simple mode, a leading `:` means
|
||||
// treat *this single submission* as advanced. The persistent
|
||||
// mode is unchanged.
|
||||
let (effective_mode, effective_input) =
|
||||
// mode is unchanged. The three-way `EffectiveMode` (ADR-0037) is
|
||||
// carried through dispatch so the runtime can gate the DSL → SQL
|
||||
// teaching echo (ADR-0038) on an advanced effective mode.
|
||||
let (submission_mode, effective_input) =
|
||||
if self.mode == Mode::Simple && trimmed.starts_with(':') {
|
||||
(Mode::Advanced, trimmed[1..].trim().to_string())
|
||||
(EffectiveMode::AdvancedOneShot, trimmed[1..].trim().to_string())
|
||||
} else if self.mode == Mode::Advanced {
|
||||
(EffectiveMode::AdvancedPersistent, trimmed.to_string())
|
||||
} else {
|
||||
(self.mode, trimmed.to_string())
|
||||
(EffectiveMode::Simple, trimmed.to_string())
|
||||
};
|
||||
|
||||
if effective_input.is_empty() {
|
||||
@@ -1143,7 +1158,7 @@ impl App {
|
||||
// form in advanced mode runs and a SQL form in simple
|
||||
// mode yields the precise "this is SQL" hint through the
|
||||
// walker's mode gate — no separate placeholder branch.
|
||||
self.dispatch_dsl(&effective_input, effective_mode)
|
||||
self.dispatch_dsl(&effective_input, submission_mode)
|
||||
}
|
||||
|
||||
/// Dispatch a parsed app-lifecycle command. Works in both
|
||||
@@ -1237,7 +1252,11 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_dsl(&mut self, input: &str, submission_mode: Mode) -> Vec<Action> {
|
||||
fn dispatch_dsl(&mut self, input: &str, submission_mode: EffectiveMode) -> Vec<Action> {
|
||||
// The two-way mode the walker + the `[mode]` render tag read; the
|
||||
// three-way `submission_mode` (ADR-0037) rides on `ExecuteDsl` for
|
||||
// the runtime's echo gate (ADR-0038).
|
||||
let mode = submission_mode.as_mode();
|
||||
// ADR-0024 §Phase D: parse with the live schema so typed
|
||||
// value slots (insert-into-T-values-…) dispatch on the
|
||||
// column's actual user-facing type instead of accepting
|
||||
@@ -1250,7 +1269,7 @@ impl App {
|
||||
match crate::dsl::parser::parse_command_with_schema_in_mode(
|
||||
input,
|
||||
&self.schema_cache,
|
||||
submission_mode,
|
||||
mode,
|
||||
) {
|
||||
Ok(Command::Replay { path }) => {
|
||||
// `replay` is parsed as a DSL command for the
|
||||
@@ -1270,7 +1289,7 @@ impl App {
|
||||
self.push_output(OutputLine {
|
||||
text: crate::t!("dsl.running", input = input),
|
||||
kind: OutputKind::Echo,
|
||||
mode_at_submission: submission_mode,
|
||||
mode_at_submission: mode,
|
||||
styled_runs: None,
|
||||
});
|
||||
vec![Action::Replay { path }]
|
||||
@@ -1279,12 +1298,13 @@ impl App {
|
||||
self.push_output(OutputLine {
|
||||
text: crate::t!("dsl.running", input = input),
|
||||
kind: OutputKind::Echo,
|
||||
mode_at_submission: submission_mode,
|
||||
mode_at_submission: mode,
|
||||
styled_runs: None,
|
||||
});
|
||||
vec![Action::ExecuteDsl {
|
||||
command: cmd,
|
||||
source: input.to_string(),
|
||||
submission_mode,
|
||||
}]
|
||||
}
|
||||
Err(ParseError::Empty) => Vec::new(),
|
||||
@@ -1294,7 +1314,7 @@ impl App {
|
||||
self.push_output(OutputLine {
|
||||
text: crate::t!("dsl.running", input = input),
|
||||
kind: OutputKind::Echo,
|
||||
mode_at_submission: submission_mode,
|
||||
mode_at_submission: mode,
|
||||
styled_runs: None,
|
||||
});
|
||||
// Caret pointer at the failure position, when we
|
||||
@@ -1334,7 +1354,7 @@ impl App {
|
||||
// covers SQL constructs that surface only on submit
|
||||
// (e.g. `delete … returning`, where the live hint
|
||||
// shows WHERE-completion rather than an error).
|
||||
if submission_mode == Mode::Simple
|
||||
if mode == Mode::Simple
|
||||
&& let Some(note) =
|
||||
crate::input_render::advanced_alternative_note(input, &self.schema_cache)
|
||||
{
|
||||
@@ -1367,6 +1387,12 @@ impl App {
|
||||
verb = command.verb(),
|
||||
subject = command.display_subject()
|
||||
));
|
||||
// ADR-0038: the DSL → SQL teaching echo, beneath `[ok]`. Set on
|
||||
// the success event when a DSL-form command ran in an advanced
|
||||
// effective mode (ADR-0037); `None` otherwise. De-emphasised.
|
||||
if let Some(sql) = self.pending_echo.take() {
|
||||
self.note_system(crate::t!("echo.executing_sql", sql = sql));
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_dsl_success(&mut self, command: &Command, description: Option<TableDescription>) {
|
||||
@@ -2789,6 +2815,70 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn submit_carries_the_three_way_effective_submission_mode() {
|
||||
// ADR-0037: ExecuteDsl carries the effective mode so the runtime
|
||||
// can gate the teaching echo (ADR-0038).
|
||||
let case = |mode: Mode, input: &str| -> EffectiveMode {
|
||||
let mut app = App::new();
|
||||
app.mode = mode;
|
||||
type_str(&mut app, input);
|
||||
match submit(&mut app).as_slice() {
|
||||
[Action::ExecuteDsl { submission_mode, .. }] => *submission_mode,
|
||||
other => panic!("expected one ExecuteDsl; got {other:?}"),
|
||||
}
|
||||
};
|
||||
assert_eq!(
|
||||
case(Mode::Advanced, "create table T with pk"),
|
||||
EffectiveMode::AdvancedPersistent
|
||||
);
|
||||
assert_eq!(
|
||||
case(Mode::Simple, ":create table T with pk"),
|
||||
EffectiveMode::AdvancedOneShot
|
||||
);
|
||||
assert_eq!(
|
||||
case(Mode::Simple, "create table T with pk"),
|
||||
EffectiveMode::Simple
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dsl_success_renders_the_teaching_echo_beneath_ok() {
|
||||
// ADR-0038: the echo carried on the success event renders as a
|
||||
// line immediately beneath the `[ok]` summary.
|
||||
let cmd = Command::CreateTable {
|
||||
name: "Other".to_string(),
|
||||
columns: vec![crate::dsl::ColumnSpec::new("id", Type::Serial)],
|
||||
primary_key: vec!["id".to_string()],
|
||||
};
|
||||
let mut app = App::new();
|
||||
app.update(AppEvent::DslSucceeded {
|
||||
command: cmd.clone(),
|
||||
description: None,
|
||||
echo: Some("CREATE TABLE Other (id serial PRIMARY KEY)".to_string()),
|
||||
});
|
||||
let texts: Vec<&str> = app.output.iter().map(|l| l.text.as_str()).collect();
|
||||
let ok_idx = texts.iter().position(|t| t.starts_with("[ok]")).expect("an [ok] line");
|
||||
let echo_idx = texts
|
||||
.iter()
|
||||
.position(|t| t.contains("Executing SQL:"))
|
||||
.expect("an echo line");
|
||||
assert_eq!(echo_idx, ok_idx + 1, "echo sits immediately beneath [ok]: {texts:?}");
|
||||
assert!(texts[echo_idx].contains("CREATE TABLE Other (id serial PRIMARY KEY)"));
|
||||
|
||||
// No echo line when the event carries none (simple mode etc.).
|
||||
let mut app = App::new();
|
||||
app.update(AppEvent::DslSucceeded {
|
||||
command: cmd,
|
||||
description: None,
|
||||
echo: None,
|
||||
});
|
||||
assert!(
|
||||
!app.output.iter().any(|l| l.text.contains("Executing SQL:")),
|
||||
"no echo line when echo is None"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mode_command_switches_persistently() {
|
||||
let mut app = App::new();
|
||||
@@ -2960,6 +3050,7 @@ mod tests {
|
||||
app.update(AppEvent::DslSucceeded {
|
||||
command: cmd,
|
||||
description: Some(desc.clone()),
|
||||
echo: None,
|
||||
});
|
||||
assert_eq!(app.current_table, Some(desc));
|
||||
// Some line in the output buffer is the structure
|
||||
|
||||
Reference in New Issue
Block a user