feat: DSL→SQL teaching echo — Bucket A renderer (ADR-0038 Phase 1)
Expands the renderer skeleton from ADR-0038's first slice to the full single-statement catalogue. Every Bucket A row round-trips through the advanced-mode walker (the §1 copy-paste contract): add column / drop column (non-cascade) / rename column / change column (SET DATA TYPE) / add constraint (not null, default, unique, check) / drop constraint (not null, default) / show data [where] [limit] / delete --all-rows / update --all-rows Adds the Expr→SQL and Value→SQL-literal renderers (ADR-0038 §5) — bare identifiers, inlined literals, NULL uppercase, standard <> for inequality — and threads `echo: Option<String>` onto the six remaining success events (DslAddColumn/DropColumn/ChangeColumn/Data/Update/Delete Succeeded) with matching runtime construction and App stash arms. `show data` is the one Bucket A row whose echo needs schema info beyond the Command (the `ORDER BY <pk>` for a limited query): the pure renderer takes the primary key as a parameter, and the runtime sources it post-execution via describe_table — gated on advanced mode + limit present, mirroring the enrich_dsl_failure describe pattern. An end-to-end test pins the describe→PK→ORDER BY glue against a real worker; the simple-mode gate and unlimited-no-lookup paths are covered too. Also fixes a contract gap surfaced while completing the catalogue: the existing create-table echo silently dropped per-column DEFAULT / CHECK, which simple-mode `create table … with pk c(ty) check (…)` does parse (ADR-0029) — so the echo was non-equivalent. The render now emits the full ADR-0029 column-constraint suffix, sharing one append_constraints helper with `add column`. Phase 2 (Bucket B — resolved-name + multi-line echoes, including `add index`), Phase 3 (category-3 prose), and the de-emphasised styled-runs polish remain deferred per ADR-0038 §8 phasing. Tests: 2000 passed / 0 failed / 1 ignored (pre-existing); clippy clean (`--all-targets -D warnings`, nursery).
This commit is contained in:
+179
-6
@@ -498,7 +498,14 @@ impl App {
|
||||
self.note_system(crate::t!("ddl.create_index_skipped_exists", name = name));
|
||||
Vec::new()
|
||||
}
|
||||
AppEvent::DslDataSucceeded { command, data } => {
|
||||
AppEvent::DslDataSucceeded {
|
||||
command,
|
||||
data,
|
||||
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_query_success(&command, &data);
|
||||
Vec::new()
|
||||
}
|
||||
@@ -510,23 +517,48 @@ impl App {
|
||||
self.handle_dsl_insert_success(&command, &result);
|
||||
Vec::new()
|
||||
}
|
||||
AppEvent::DslUpdateSucceeded { command, result } => {
|
||||
AppEvent::DslUpdateSucceeded {
|
||||
command,
|
||||
result,
|
||||
echo,
|
||||
} => {
|
||||
self.pending_echo = echo;
|
||||
self.handle_dsl_update_success(&command, &result);
|
||||
Vec::new()
|
||||
}
|
||||
AppEvent::DslDeleteSucceeded { command, result } => {
|
||||
AppEvent::DslDeleteSucceeded {
|
||||
command,
|
||||
result,
|
||||
echo,
|
||||
} => {
|
||||
self.pending_echo = echo;
|
||||
self.handle_dsl_delete_success(&command, &result);
|
||||
Vec::new()
|
||||
}
|
||||
AppEvent::DslChangeColumnSucceeded { command, result } => {
|
||||
AppEvent::DslChangeColumnSucceeded {
|
||||
command,
|
||||
result,
|
||||
echo,
|
||||
} => {
|
||||
self.pending_echo = echo;
|
||||
self.handle_dsl_change_column_success(&command, result);
|
||||
Vec::new()
|
||||
}
|
||||
AppEvent::DslAddColumnSucceeded { command, result } => {
|
||||
AppEvent::DslAddColumnSucceeded {
|
||||
command,
|
||||
result,
|
||||
echo,
|
||||
} => {
|
||||
self.pending_echo = echo;
|
||||
self.handle_dsl_add_column_success(&command, result);
|
||||
Vec::new()
|
||||
}
|
||||
AppEvent::DslDropColumnSucceeded { command, result } => {
|
||||
AppEvent::DslDropColumnSucceeded {
|
||||
command,
|
||||
result,
|
||||
echo,
|
||||
} => {
|
||||
self.pending_echo = echo;
|
||||
self.handle_dsl_drop_column_success(&command, result);
|
||||
Vec::new()
|
||||
}
|
||||
@@ -2879,6 +2911,143 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bucket_a_success_events_render_the_teaching_echo_beneath_ok() {
|
||||
// ADR-0038 Phase 1: every Bucket A success event that gained an
|
||||
// `echo` field stashes it for `note_ok_summary` to render
|
||||
// immediately beneath `[ok]`. One case per event guards its
|
||||
// update() arm's stash + ordering — including the handlers that
|
||||
// render extra content (structure / notes), where the echo must
|
||||
// still sit directly beneath `[ok]`, above that content.
|
||||
use crate::db::{
|
||||
AddColumnResult, ChangeColumnTypeResult, DataResult, DeleteResult, DropColumnResult,
|
||||
UpdateResult,
|
||||
};
|
||||
use crate::dsl::command::{ChangeColumnMode, RowFilter};
|
||||
use crate::dsl::value::Value;
|
||||
|
||||
fn assert_echo_beneath_ok(app: &App, expected: &str) {
|
||||
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(expected), "echo carries the SQL: {texts:?}");
|
||||
}
|
||||
|
||||
fn empty_data() -> DataResult {
|
||||
DataResult {
|
||||
table_name: "T".to_string(),
|
||||
columns: Vec::new(),
|
||||
column_types: Vec::new(),
|
||||
rows: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
// show data → DslDataSucceeded (the post-execution query path).
|
||||
let mut app = App::new();
|
||||
app.update(AppEvent::DslDataSucceeded {
|
||||
command: Command::ShowData {
|
||||
name: "T".to_string(),
|
||||
filter: None,
|
||||
limit: None,
|
||||
},
|
||||
data: empty_data(),
|
||||
echo: Some("SELECT * FROM T".to_string()),
|
||||
});
|
||||
assert_echo_beneath_ok(&app, "SELECT * FROM T");
|
||||
|
||||
// update … --all-rows → DslUpdateSucceeded.
|
||||
let mut app = App::new();
|
||||
app.update(AppEvent::DslUpdateSucceeded {
|
||||
command: Command::Update {
|
||||
table: "T".to_string(),
|
||||
assignments: vec![("v".to_string(), Value::Number("1".to_string()))],
|
||||
filter: RowFilter::AllRows,
|
||||
},
|
||||
result: UpdateResult {
|
||||
rows_affected: 1,
|
||||
data: empty_data(),
|
||||
},
|
||||
echo: Some("UPDATE T SET v = 1".to_string()),
|
||||
});
|
||||
assert_echo_beneath_ok(&app, "UPDATE T SET v = 1");
|
||||
|
||||
// delete … --all-rows → DslDeleteSucceeded.
|
||||
let mut app = App::new();
|
||||
app.update(AppEvent::DslDeleteSucceeded {
|
||||
command: Command::Delete {
|
||||
table: "T".to_string(),
|
||||
filter: RowFilter::AllRows,
|
||||
},
|
||||
result: DeleteResult {
|
||||
rows_affected: 1,
|
||||
cascade: Vec::new(),
|
||||
data: empty_data(),
|
||||
},
|
||||
echo: Some("DELETE FROM T".to_string()),
|
||||
});
|
||||
assert_echo_beneath_ok(&app, "DELETE FROM T");
|
||||
|
||||
// add column → DslAddColumnSucceeded (handler also renders structure).
|
||||
let mut app = App::new();
|
||||
app.update(AppEvent::DslAddColumnSucceeded {
|
||||
command: Command::AddColumn {
|
||||
table: "T".to_string(),
|
||||
column: "c".to_string(),
|
||||
ty: Type::Int,
|
||||
not_null: false,
|
||||
unique: false,
|
||||
default: None,
|
||||
check: None,
|
||||
},
|
||||
result: AddColumnResult {
|
||||
description: sample_description("T"),
|
||||
client_side_notes: Vec::new(),
|
||||
},
|
||||
echo: Some("ALTER TABLE T ADD COLUMN c int".to_string()),
|
||||
});
|
||||
assert_echo_beneath_ok(&app, "ALTER TABLE T ADD COLUMN c int");
|
||||
|
||||
// drop column → DslDropColumnSucceeded.
|
||||
let mut app = App::new();
|
||||
app.update(AppEvent::DslDropColumnSucceeded {
|
||||
command: Command::DropColumn {
|
||||
table: "T".to_string(),
|
||||
column: "c".to_string(),
|
||||
cascade: false,
|
||||
},
|
||||
result: DropColumnResult {
|
||||
description: sample_description("T"),
|
||||
dropped_indexes: Vec::new(),
|
||||
},
|
||||
echo: Some("ALTER TABLE T DROP COLUMN c".to_string()),
|
||||
});
|
||||
assert_echo_beneath_ok(&app, "ALTER TABLE T DROP COLUMN c");
|
||||
|
||||
// change column → DslChangeColumnSucceeded.
|
||||
let mut app = App::new();
|
||||
app.update(AppEvent::DslChangeColumnSucceeded {
|
||||
command: Command::ChangeColumnType {
|
||||
table: "T".to_string(),
|
||||
column: "c".to_string(),
|
||||
ty: Type::Text,
|
||||
mode: ChangeColumnMode::Default,
|
||||
},
|
||||
result: ChangeColumnTypeResult {
|
||||
description: sample_description("T"),
|
||||
client_side: None,
|
||||
},
|
||||
echo: Some("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 mode_command_switches_persistently() {
|
||||
let mut app = App::new();
|
||||
@@ -3982,6 +4151,7 @@ mod tests {
|
||||
rows: Vec::new(),
|
||||
},
|
||||
},
|
||||
echo: None,
|
||||
});
|
||||
let texts: Vec<String> = app.output.iter().map(|l| l.text.clone()).collect();
|
||||
assert!(
|
||||
@@ -4015,6 +4185,7 @@ mod tests {
|
||||
rows: vec![vec![Some("1".to_string()), Some("9".to_string())]],
|
||||
},
|
||||
},
|
||||
echo: None,
|
||||
});
|
||||
let texts: Vec<String> = app.output.iter().map(|l| l.text.clone()).collect();
|
||||
assert!(
|
||||
@@ -4055,6 +4226,7 @@ mod tests {
|
||||
rows: Vec::new(),
|
||||
},
|
||||
},
|
||||
echo: None,
|
||||
});
|
||||
let texts: Vec<String> = app.output.iter().map(|l| l.text.clone()).collect();
|
||||
assert!(
|
||||
@@ -4102,6 +4274,7 @@ mod tests {
|
||||
rows: vec![vec![Some("1".to_string()), Some("Alice".to_string())]],
|
||||
},
|
||||
},
|
||||
echo: None,
|
||||
});
|
||||
let texts: Vec<String> = app.output.iter().map(|l| l.text.clone()).collect();
|
||||
assert!(
|
||||
|
||||
Reference in New Issue
Block a user