feat: DSL→SQL teaching echo — Bucket B renderer (ADR-0038 Phase 2)
Expands the renderer to Bucket B — resolved-name single-statement
echoes plus the two category-2 multi-statement forms. Every catalogue
row round-trips per line through the advanced-mode walker (the §1
copy-paste contract; §6 category 2 holds the contract per line):
add index [as N] on T (cols) → CREATE INDEX <name> ON T (cols)
drop index on T (cols) (positional) → DROP INDEX <name>
add 1:n relationship [as N] … → ALTER TABLE C ADD CONSTRAINT
<name> FOREIGN KEY (cc)
REFERENCES P (pc) [ON …]
drop relationship (endpoints or named) → ALTER TABLE C DROP CONSTRAINT
<name>
drop column T.c --cascade → DROP INDEX <ix1> ⏎ … ⏎
ALTER TABLE T DROP COLUMN c
add relationship … --create-fk → ALTER TABLE C ADD COLUMN cc <ty>
(child column newly created) ⏎ ALTER TABLE … ADD CONSTRAINT
(already existed) collapses to a single-line FK echo
Refactors the echo payload from Option<String> to Option<Vec<String>>
across the 7 success events + arms + render path — one entry per
statement; the Bucket A single-line echoes wrap as Some(vec![s]). Plain
rendering repeats `Executing SQL:` per line; the de-emphasised
styled-runs polish (ADR-0038 §4) will refine it later.
Adds the two echo build paths the handoff §5 ⚠️ gotcha foreshadowed:
* collect_echo_lookups (pre-execution, runtime): resolves names the
dropped thing or not-yet-created column would erase post-execution —
drop index (positional), drop relationship (both endpoints and named
selectors, the latter via a list_tables scan acceptable for teaching-
playground schemas), and the --create-fk pre-state (whether the child
column existed + the parent PK type to derive the new column type via
Type::fk_target_type).
* build_schema_echo (post-execution, runtime): subsumes the Bucket A
pure-Command schema cases and renders Bucket B from the description +
the lookups.
The DropColumn arm gains build_drop_column_cascade_echo, which reads
DropColumnResult.dropped_indexes to emit the multi-line cascade echo;
non-cascade falls through to the pre-execution Bucket A echo unchanged.
Tests: 2013 passed / 0 failed / 1 ignored (pre-existing); clippy clean
(`--all-targets -D warnings`, nursery). Two end-to-end runtime tests
exercise the resolved-name and multi-statement flows against a real
worker (auto-named index, both drop-relationship selector forms, both
--create-fk branches). One app-level test pins the multi-line rendering
(one Executing SQL: per statement, in order, beneath [ok]).
Phase 3 (category-3 prose — shortid generation, type-conversion
transforms, `change column --dont-convert` caveat) and the §4
de-emphasised styled-runs rendering polish remain per ADR-0038 §8
phasing.
This commit is contained in:
+73
-11
@@ -221,7 +221,7 @@ pub struct App {
|
||||
/// 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>,
|
||||
pending_echo: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
/// Dialogs that take over keyboard input when active.
|
||||
@@ -1421,9 +1421,15 @@ impl App {
|
||||
));
|
||||
// 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));
|
||||
// effective mode (ADR-0037); `None` otherwise. De-emphasised
|
||||
// (styled-runs polish per ADR-0038 §4 still pending). One line
|
||||
// per statement — single-statement echoes render one line;
|
||||
// multi-statement (`drop column --cascade`, `add relationship
|
||||
// --create-fk`) render one per entry (ADR-0038 §6 category 2).
|
||||
if let Some(lines) = self.pending_echo.take() {
|
||||
for line in lines {
|
||||
self.note_system(crate::t!("echo.executing_sql", sql = line));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2887,7 +2893,7 @@ mod tests {
|
||||
app.update(AppEvent::DslSucceeded {
|
||||
command: cmd.clone(),
|
||||
description: None,
|
||||
echo: Some("CREATE TABLE Other (id serial PRIMARY KEY)".to_string()),
|
||||
echo: Some(vec!["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");
|
||||
@@ -2958,7 +2964,7 @@ mod tests {
|
||||
limit: None,
|
||||
},
|
||||
data: empty_data(),
|
||||
echo: Some("SELECT * FROM T".to_string()),
|
||||
echo: Some(vec!["SELECT * FROM T".to_string()]),
|
||||
});
|
||||
assert_echo_beneath_ok(&app, "SELECT * FROM T");
|
||||
|
||||
@@ -2974,7 +2980,7 @@ mod tests {
|
||||
rows_affected: 1,
|
||||
data: empty_data(),
|
||||
},
|
||||
echo: Some("UPDATE T SET v = 1".to_string()),
|
||||
echo: Some(vec!["UPDATE T SET v = 1".to_string()]),
|
||||
});
|
||||
assert_echo_beneath_ok(&app, "UPDATE T SET v = 1");
|
||||
|
||||
@@ -2990,7 +2996,7 @@ mod tests {
|
||||
cascade: Vec::new(),
|
||||
data: empty_data(),
|
||||
},
|
||||
echo: Some("DELETE FROM T".to_string()),
|
||||
echo: Some(vec!["DELETE FROM T".to_string()]),
|
||||
});
|
||||
assert_echo_beneath_ok(&app, "DELETE FROM T");
|
||||
|
||||
@@ -3010,7 +3016,7 @@ mod tests {
|
||||
description: sample_description("T"),
|
||||
client_side_notes: Vec::new(),
|
||||
},
|
||||
echo: Some("ALTER TABLE T ADD COLUMN c int".to_string()),
|
||||
echo: Some(vec!["ALTER TABLE T ADD COLUMN c int".to_string()]),
|
||||
});
|
||||
assert_echo_beneath_ok(&app, "ALTER TABLE T ADD COLUMN c int");
|
||||
|
||||
@@ -3026,7 +3032,7 @@ mod tests {
|
||||
description: sample_description("T"),
|
||||
dropped_indexes: Vec::new(),
|
||||
},
|
||||
echo: Some("ALTER TABLE T DROP COLUMN c".to_string()),
|
||||
echo: Some(vec!["ALTER TABLE T DROP COLUMN c".to_string()]),
|
||||
});
|
||||
assert_echo_beneath_ok(&app, "ALTER TABLE T DROP COLUMN c");
|
||||
|
||||
@@ -3043,11 +3049,67 @@ mod tests {
|
||||
description: sample_description("T"),
|
||||
client_side: None,
|
||||
},
|
||||
echo: Some("ALTER TABLE T ALTER COLUMN c SET DATA TYPE text".to_string()),
|
||||
echo: Some(vec!["ALTER TABLE T ALTER COLUMN c SET DATA TYPE text".to_string()]),
|
||||
});
|
||||
assert_echo_beneath_ok(&app, "ALTER TABLE T ALTER COLUMN c SET DATA TYPE text");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bucket_b_multi_line_echo_renders_one_line_per_statement_beneath_ok() {
|
||||
// ADR-0038 §6 category 2 / §4 / Phase 2 Slice 2b: a `drop column
|
||||
// --cascade` echo carries one `DROP INDEX <name>` line per
|
||||
// covering index plus the final `ALTER TABLE … DROP COLUMN …`.
|
||||
// The App renders each as its own `Executing SQL:` line beneath
|
||||
// `[ok]`, in order — the styled-runs polish refines the
|
||||
// presentation later, but ordering and one-per-statement are the
|
||||
// semantic invariants pinned here.
|
||||
use crate::db::DropColumnResult;
|
||||
|
||||
let mut app = App::new();
|
||||
app.update(AppEvent::DslDropColumnSucceeded {
|
||||
command: Command::DropColumn {
|
||||
table: "Customers".to_string(),
|
||||
column: "Email".to_string(),
|
||||
cascade: true,
|
||||
},
|
||||
result: DropColumnResult {
|
||||
description: sample_description("Customers"),
|
||||
dropped_indexes: vec![
|
||||
"Customers_Email_idx".to_string(),
|
||||
"Customers_Email_Day_idx".to_string(),
|
||||
],
|
||||
},
|
||||
echo: Some(vec![
|
||||
"DROP INDEX Customers_Email_idx".to_string(),
|
||||
"DROP INDEX Customers_Email_Day_idx".to_string(),
|
||||
"ALTER TABLE Customers DROP COLUMN Email".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");
|
||||
// The three echo lines sit immediately beneath [ok], in order.
|
||||
assert!(
|
||||
texts[ok_idx + 1].contains("Executing SQL: DROP INDEX Customers_Email_idx"),
|
||||
"first echo line: {texts:?}",
|
||||
);
|
||||
assert!(
|
||||
texts[ok_idx + 2].contains("Executing SQL: DROP INDEX Customers_Email_Day_idx"),
|
||||
"second echo line: {texts:?}",
|
||||
);
|
||||
assert!(
|
||||
texts[ok_idx + 3]
|
||||
.contains("Executing SQL: ALTER TABLE Customers DROP COLUMN Email"),
|
||||
"third echo line: {texts:?}",
|
||||
);
|
||||
// Pin the `Executing SQL:` prefix repeats once per statement
|
||||
// (the plain-rendering shape until the styled-runs polish lands).
|
||||
let exec_count = texts.iter().filter(|t| t.contains("Executing SQL:")).count();
|
||||
assert_eq!(exec_count, 3, "one Executing SQL: per statement: {texts:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mode_command_switches_persistently() {
|
||||
let mut app = App::new();
|
||||
|
||||
Reference in New Issue
Block a user