diff --git a/docs/adr/0037-execution-time-mode-side-channel.md b/docs/adr/0037-execution-time-mode-side-channel.md index 0546563..b9eda0f 100644 --- a/docs/adr/0037-execution-time-mode-side-channel.md +++ b/docs/adr/0037-execution-time-mode-side-channel.md @@ -109,20 +109,24 @@ effective mode it ran under. The value is **output-only**: no executor branches its *effect* on it (that would be a behavioural mode dependency, which ADR-0033 Amendment 3 forbids — identity and effect are intrinsic). -### 3. The worker produces mode-dependent output; the App renders it +### 3. The runtime's execution dispatcher produces the echo; the App renders it For the first consumer (ADR-0038): when the command is a **DSL-form** command (`Command::CreateTable`/`Insert`/… — *not* the `Sql*` variants) -and `submission_mode` is `Advanced` or `AdvancedOneShot`, the worker -builds the teaching echo (equivalent SQL + any category-3 expansion -data — ADR-0038) and returns it on the result event. In `Simple` mode, -or for a command typed as SQL, no echo is produced. The App renders the -returned echo as de-emphasised `OutputLine`(s) beneath `[ok]`. +and `submission_mode` is `Advanced` or `AdvancedOneShot`, the teaching +echo (equivalent SQL + any category-3 expansion data — ADR-0038) is built +from the `Command` **plus the worker's execution result**, and the App +renders it as de-emphasised `OutputLine`(s) beneath `[ok]`. In `Simple` +mode, or for a command typed as SQL, no echo is produced. -Co-locating echo construction with execution is deliberate: the echo's -harder forms (resolved auto-names, generated `shortid`s, conversion -counts) are facts the worker already computes. Gating on the threaded -mode means the work happens **only when an echo will be shown**. +**Where it is built (build correction — see Implementation notes).** Not +in the db.rs worker: the worker receives *decomposed* calls, not the +`Command`, so it cannot render `Command → SQL`. The echo is built at the +**runtime's `ExecuteDsl` handler**, the one place where the `Command`, +the threaded `EffectiveMode`, and the worker's result (resolved +auto-names, generated `shortid`s, conversion counts) all converge. This +is still **execution-time aware** — it consumes the execution *results* — +it just lives at the dispatch layer, not inside the storage worker. **Non-interactive re-execution does not echo.** `replay` (ADR-0034) re-runs recorded commands through the dispatch pipeline in advanced mode @@ -172,6 +176,26 @@ the gating contract in §3. `Action` → worker round-trip; a Simple-mode DSL command yields no echo request while an Advanced / one-shot one does (the gating contract). +## Implementation notes (2026-05-27, during build) + +Two refinements found when building, recorded so the ADR matches reality: + +- **Reuse the existing `EffectiveMode`, do not add `SubmissionMode`.** The + codebase already has `EffectiveMode { Simple, AdvancedPersistent, + AdvancedOneShot }` (`app.rs`), computed by `effective_mode()` and used + today for the `:` one-shot UI feedback. It is exactly the three-way, + per-submission, *separate-from-`Mode`* enum §1 argued for — so §1's + "new enum" is already satisfied; the build reuses `EffectiveMode` + (`AdvancedPersistent` is the ADR's `Advanced`). No new type. +- **The channel ships with its consumer (merged with ADR-0038).** A + threaded-but-unread `EffectiveMode` on the worker request is dead code, + which this project's `-D warnings` (nursery) rejects. The side-channel + has no consumer other than the echo, so the `Action`→worker threading + is built **together with ADR-0038** rather than as a standalone commit + — the submit-side resolution (which `Action` carries which + `EffectiveMode`) is Tier-1 testable, and the worker-side threading + becomes live + end-to-end testable the moment the echo reads it. + ## See also - ADR-0033 Amendment 3 — deferred this side-channel; defines the diff --git a/docs/adr/0038-dsl-to-sql-teaching-echo.md b/docs/adr/0038-dsl-to-sql-teaching-echo.md index 56a1e95..7233c40 100644 --- a/docs/adr/0038-dsl-to-sql-teaching-echo.md +++ b/docs/adr/0038-dsl-to-sql-teaching-echo.md @@ -97,13 +97,17 @@ echoes would bury the replay summary — ADR-0037 §3). ### 4. Where it is built and rendered -The **worker builds** the echo (ADR-0037 §3) — it alone holds the facts -several echoes need: auto-resolved index / relationship names, generated -`shortid` values, and lossy-conversion counts. It returns the echo -payload on the result event. The **App renders** it as one or more -**de-emphasised** `OutputLine`s beneath the `[ok]` summary, using the -ADR-0028 styled-runs mechanism (a dimmed `Executing SQL:` prefix; the SQL -itself in a code-ish run). One statement per line (§6 category 2). +The echo is built at the **runtime's `ExecuteDsl` handler** (build +correction, ADR-0037 §3 + Implementation notes): the db.rs worker +receives *decomposed* calls, not the `Command`, so it cannot render +`Command → SQL`. The runtime is the one place where the `Command`, the +threaded `EffectiveMode`, and the worker's **result** (resolved auto- +names, generated `shortid`s, conversion counts) all converge — so it +builds the echo there, still **execution-time aware** (it consumes the +results). The **App renders** it as one or more **de-emphasised** +`OutputLine`s beneath the `[ok]` summary, using the ADR-0028 styled-runs +mechanism (a dimmed `Executing SQL:` prefix; the SQL in a code-ish run). +One statement per line (§6 category 2). ### 5. `Value → SQL-literal` rendering diff --git a/src/action.rs b/src/action.rs index acb890b..d08d68c 100644 --- a/src/action.rs +++ b/src/action.rs @@ -7,6 +7,7 @@ //! itself, which keeps update directly testable without a Tokio //! runtime, a real terminal, or a database. +use crate::app::EffectiveMode; use crate::dsl::Command; #[derive(Debug, Clone, PartialEq, Eq)] @@ -23,6 +24,12 @@ pub enum Action { ExecuteDsl { command: Command, source: String, + /// The effective mode the line was submitted under (ADR-0037): + /// `Simple` / `AdvancedPersistent` / `AdvancedOneShot`. Output-only + /// — execution semantics do not depend on it; the runtime uses it + /// to gate the DSL → SQL teaching echo (ADR-0038), which fires for + /// DSL-form commands submitted in an advanced effective mode. + submission_mode: EffectiveMode, }, /// Record a *failed* submission to `history.log` as an `err` /// record (ADR-0034 §1/§2). Emitted by the pure-sync `App` diff --git a/src/app.rs b/src/app.rs index 5ea7723..581fc6d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -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, } /// 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 { + fn dispatch_dsl(&mut self, input: &str, submission_mode: EffectiveMode) -> Vec { + // 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) { @@ -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 diff --git a/src/echo.rs b/src/echo.rs new file mode 100644 index 0000000..a3592c8 --- /dev/null +++ b/src/echo.rs @@ -0,0 +1,160 @@ +//! The DSL → SQL teaching echo renderer (ADR-0038). +//! +//! Maps a **DSL-form** `Command` to the equivalent advanced-mode SQL, so +//! a learner who typed the simple-mode form reads off how to spell it in +//! SQL (ADR-0030 §10). The output obeys the **copy-paste contract** +//! (ADR-0038 §1): it is runnable advanced-mode SQL in the playground's +//! own type vocabulary (`Type::keyword()`), so it round-trips through the +//! advanced walker. The standard-first dialect (ADR-0035 Amendment 2) +//! governs statement shape; the playground keywords fill the type slots. +//! +//! `None` means "no echo" — a command in Bucket C (ADR-0038 §7) or a form +//! not yet covered by the renderer. The caller (the runtime's `ExecuteDsl` +//! dispatch) only invokes this for DSL-form commands submitted in an +//! advanced effective mode (ADR-0037). + +use crate::app::EffectiveMode; +use crate::dsl::Command; +use crate::dsl::command::ColumnSpec; + +/// The teaching echo for a command submitted under `mode`, or `None`. +/// +/// Fires only in an advanced effective mode (`AdvancedPersistent` / +/// `AdvancedOneShot`) — simple mode stays uncluttered (ADR-0030 §10) — and +/// only for a DSL-form command that has an echo (`command_to_sql`; a +/// `Sql*` / app command returns `None`). This is the runtime's gate; +/// replay never reaches it (it bypasses the spawn). (ADR-0037 + ADR-0038) +#[must_use] +pub fn echo_for(command: &Command, mode: EffectiveMode) -> Option { + if mode.is_advanced() { + command_to_sql(command) + } else { + None + } +} + +/// Render the equivalent advanced-mode SQL for a DSL-form command, or +/// `None` when it has no echo. +#[must_use] +pub fn command_to_sql(command: &Command) -> Option { + match command { + Command::CreateTable { + name, + columns, + primary_key, + } => Some(render_create_table(name, columns, primary_key)), + // Remaining Bucket A/B forms land in follow-up slices (ADR-0038 §8). + _ => None, + } +} + +/// `CREATE TABLE ([, PRIMARY KEY (…)])` in the +/// playground's type vocabulary. A single first-column primary key is +/// rendered inline (`id serial PRIMARY KEY`), matching the rebuild +/// generator's rule (ADR-0035 §4a) and the round-trip surface; a compound +/// key becomes a table-level `PRIMARY KEY (a, b)`. +fn render_create_table(name: &str, columns: &[ColumnSpec], primary_key: &[String]) -> String { + let inline_pk = + primary_key.len() == 1 && columns.first().is_some_and(|c| c.name == primary_key[0]); + let col_defs: Vec = columns + .iter() + .map(|c| { + let mut s = format!("{} {}", c.name, c.ty.keyword()); + if inline_pk && c.name == primary_key[0] { + s.push_str(" PRIMARY KEY"); + } + if c.not_null { + s.push_str(" NOT NULL"); + } + if c.unique { + s.push_str(" UNIQUE"); + } + s + }) + .collect(); + if primary_key.len() > 1 { + format!( + "CREATE TABLE {name} ({}, PRIMARY KEY ({}))", + col_defs.join(", "), + primary_key.join(", "), + ) + } else { + format!("CREATE TABLE {name} ({})", col_defs.join(", ")) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::dsl::types::Type; + + fn create_table(name: &str, cols: Vec, pk: &[&str]) -> Command { + Command::CreateTable { + name: name.to_string(), + columns: cols, + primary_key: pk.iter().map(|s| (*s).to_string()).collect(), + } + } + + #[test] + fn create_table_single_serial_pk_renders_inline() { + let cmd = create_table("Other", vec![ColumnSpec::new("id", Type::Serial)], &["id"]); + assert_eq!( + command_to_sql(&cmd).as_deref(), + Some("CREATE TABLE Other (id serial PRIMARY KEY)") + ); + } + + #[test] + fn create_table_compound_pk_renders_table_level() { + let cmd = create_table( + "T", + vec![ColumnSpec::new("a", Type::Int), ColumnSpec::new("b", Type::Int)], + &["a", "b"], + ); + assert_eq!( + command_to_sql(&cmd).as_deref(), + Some("CREATE TABLE T (a int, b int, PRIMARY KEY (a, b))") + ); + } + + #[test] + fn create_table_echo_round_trips_in_advanced_mode() { + // ADR-0038 §1 copy-paste contract: the echo is runnable advanced SQL. + let cmd = create_table("Other", vec![ColumnSpec::new("id", Type::Serial)], &["id"]); + let sql = command_to_sql(&cmd).expect("echo"); + let reparsed = crate::dsl::parser::parse_command_in_mode(&sql, crate::mode::Mode::Advanced); + assert!( + matches!(reparsed, Ok(Command::SqlCreateTable { .. })), + "echo must round-trip as runnable advanced SQL; got {reparsed:?}" + ); + } + + #[test] + fn echo_for_gates_on_advanced_mode() { + let cmd = create_table("Other", vec![ColumnSpec::new("id", Type::Serial)], &["id"]); + assert!(echo_for(&cmd, EffectiveMode::AdvancedPersistent).is_some()); + assert!(echo_for(&cmd, EffectiveMode::AdvancedOneShot).is_some()); + assert!( + echo_for(&cmd, EffectiveMode::Simple).is_none(), + "simple mode stays uncluttered (ADR-0030 §10)" + ); + } + + #[test] + fn sql_entered_command_is_not_echoed() { + // A command the user typed as SQL (SqlCreateTable) is not echoed + // back (ADR-0030 §10) — command_to_sql covers only DSL-form variants. + let cmd = Command::SqlCreateTable { + name: "T".to_string(), + columns: vec![ColumnSpec::new("id", Type::Serial)], + primary_key: vec!["id".to_string()], + unique_constraints: Vec::new(), + check_constraints: Vec::new(), + foreign_keys: Vec::new(), + if_not_exists: false, + }; + assert!(command_to_sql(&cmd).is_none()); + assert!(echo_for(&cmd, EffectiveMode::AdvancedPersistent).is_none()); + } +} diff --git a/src/event.rs b/src/event.rs index fad3e02..8b12ccf 100644 --- a/src/event.rs +++ b/src/event.rs @@ -27,6 +27,12 @@ pub enum AppEvent { DslSucceeded { command: Command, description: Option, + /// The DSL → SQL teaching echo (ADR-0038): equivalent advanced-mode + /// SQL, built by the runtime when a DSL-form command ran in an + /// advanced effective mode (ADR-0037). `None` when no echo applies + /// (simple mode, SQL-entered, or a form with no echo). The App + /// renders it beneath `[ok]`. + echo: Option, }, /// A SQL `CREATE TABLE IF NOT EXISTS` matched an existing table — /// a no-op (ADR-0035 §4). Renders the existing structure plus an diff --git a/src/friendly/keys.rs b/src/friendly/keys.rs index 552cd1f..aa36793 100644 --- a/src/friendly/keys.rs +++ b/src/friendly/keys.rs @@ -497,6 +497,8 @@ pub const KEYS_AND_PLACEHOLDERS: &[(&str, &[&str])] = &[ ("ok.rows_inserted", &["count"]), ("ok.rows_updated", &["count"]), ("ok.summary", &["verb", "subject"]), + // ---- DSL → SQL teaching echo (ADR-0038) ---- + ("echo.executing_sql", &["sql"]), // ---- Client-side success notes (ADR-0017 §6, ADR-0018 §9) ---- ("client_side.auto_fill_add_serial", &["count"]), ("client_side.auto_fill_add_shortid", &["count"]), diff --git a/src/friendly/strings/en-US.yaml b/src/friendly/strings/en-US.yaml index 9077dd8..ba65ed4 100644 --- a/src/friendly/strings/en-US.yaml +++ b/src/friendly/strings/en-US.yaml @@ -874,6 +874,12 @@ db: Cannot add this CHECK to `{table}.{column}`: {total} row(s) do not satisfy `{rule}`. # ---- DSL command success summaries (ADR-0019 §9 sweep) -------------- +# DSL → SQL teaching echo (ADR-0038): the equivalent advanced-mode SQL, +# rendered beneath `[ok]` for a DSL-form command run in an advanced +# effective mode (ADR-0037). +echo: + executing_sql: "Executing SQL: {sql}" + ok: # Generic `[ok] ` header used for every # successful DSL command. Verbs come from `Command::verb()` diff --git a/src/lib.rs b/src/lib.rs index 2484d48..8b06368 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,6 +12,7 @@ pub mod cli; pub mod completion; pub mod db; pub mod dsl; +pub mod echo; pub mod event; pub mod friendly; pub mod input_render; diff --git a/src/runtime.rs b/src/runtime.rs index 4682070..f2a33d9 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -384,12 +384,17 @@ async fn run_loop( debug!("quit action received"); should_quit = true; } - Action::ExecuteDsl { command, source } => { + Action::ExecuteDsl { + command, + source, + submission_mode, + } => { spawn_dsl_dispatch( session.database().clone(), event_tx.clone(), command, source, + submission_mode, ); } Action::JournalFailure { source } => { @@ -1251,16 +1256,25 @@ fn spawn_dsl_dispatch( event_tx: mpsc::Sender, command: Command, source: String, + submission_mode: crate::app::EffectiveMode, ) { tokio::spawn(async move { // Retain the source for `DslFailed` so the App can journal a // rejected command as `err` (ADR-0034 §1/§2). let source_for_journal = source.clone(); + // ADR-0038: the DSL → SQL teaching echo fires for a DSL-form + // command submitted in an advanced effective mode (ADR-0037). + // `replay` bypasses this spawn (it calls `execute_command_typed` + // directly), so replayed lines never echo. Built before execution + // from the Command; resolved-name / category-3 forms (Bucket B/§6, + // later slices) will additionally read the execution result. + let echo = crate::echo::echo_for(&command, submission_mode); let outcome = execute_command_typed(&database, command.clone(), source).await; let event = match outcome { Ok(CommandOutcome::Schema(description)) => AppEvent::DslSucceeded { command: command.clone(), description, + echo, }, Ok(CommandOutcome::SchemaSkipped(description)) => AppEvent::DslCreateSkipped { command: command.clone(), diff --git a/tests/sql_select.rs b/tests/sql_select.rs index eb778c3..791b5d8 100644 --- a/tests/sql_select.rs +++ b/tests/sql_select.rs @@ -60,6 +60,7 @@ fn advanced_mode_select_dispatches_as_command_select() { [Action::ExecuteDsl { command: Command::Select { sql }, source, + .. }] => { assert!( sql.contains("select 1"), diff --git a/tests/walking_skeleton.rs b/tests/walking_skeleton.rs index 73d5d15..f9b4241 100644 --- a/tests/walking_skeleton.rs +++ b/tests/walking_skeleton.rs @@ -294,6 +294,7 @@ fn create_table_flow_updates_tables_list_and_structure_view() { app.update(AppEvent::DslSucceeded { command: expected_cmd, description: Some(desc.clone()), + echo: None, }); app.update(AppEvent::TablesRefreshed(vec!["Customers".to_string()])); @@ -358,6 +359,7 @@ fn add_column_flow_updates_structure_view() { check: None, }, description: Some(updated.clone()), + echo: None, }); assert_eq!(app.current_table, Some(updated)); let rendered = rendered_text(&mut app, &Theme::dark(), 80, 24); @@ -387,6 +389,7 @@ fn drop_table_flow_clears_items_list() { name: "Customers".to_string(), }, description: None, + echo: None, }); app.update(AppEvent::TablesRefreshed(Vec::new())); @@ -460,6 +463,7 @@ fn add_relationship_flow_shows_parent_side_with_inbound_section() { create_fk: false, }, description: Some(customers), + echo: None, }); let rendered = rendered_text(&mut app, &Theme::dark(), 80, 24); @@ -513,6 +517,7 @@ fn add_relationship_flow_shows_inbound_section_on_parent() { check: None, }, description: Some(customers), + echo: None, }); let rendered = rendered_text(&mut app, &Theme::dark(), 80, 24); assert!(rendered.contains("Referenced by:"), "{rendered}");