diff --git a/docs/requirements.md b/docs/requirements.md index 707593e..32d92e0 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -450,13 +450,29 @@ since ADR-0027.) buffer is in, with new output snapping the view to the most recent. The full V4 scope — smart structure rendering, log styling, Markdown export, scroll indicator — remains pending.)* -- [/] **V5** `show []` family of commands for +- [x] **V5** `show []` family of commands for redisplaying schema info on demand. - *(Partial, verified 2026-06-07: `show table ` and - `show data ` implemented (`grammar/data.rs`). **Missing - the "all items" variants** — `show tables`, `show - relationships`, `show indexes` — none are registered. V5 closes - when the plural/list forms land.)* + *(Done 2026-06-07: `show table ` + `show data
` + (single-item) plus the list-all family `show tables` / + `show relationships` / `show indexes` — the latter three landed + as `Command::ShowList { kind }` (one variant, `grammar/data.rs`), + a read-only worker `show_list` formatting count-headed lists from + the same helpers the items panel uses, with help + parse-usage + entries and 10 integration tests (`tests/it/show_list.rs`). The + one remaining member of the `[]` clause — singular + per-item detail for relationships/indexes — is split out as + **V5a** below so it has a tracked home rather than living as a + footnote here.)* +- [ ] **V5a** Singular per-item detail views `show relationship + ` / `show index ` — the `[]` half of V5 for + the relationship and index kinds (the table kind already has + `show table `). Each names one item and renders its + detail (a relationship's endpoints + ON DELETE/UPDATE actions; an + index's table, columns, and uniqueness). Small follow-up on the + V5 machinery: a name slot after the `relationship` / `index` + keyword in `SHOW_CHOICES`, a lookup-one in the worker, and a + detail render. Not yet built; raised 2026-06-07 when V5's + list-all family shipped. - [x] **V6** Copy the output panel to the system clipboard (issue #11, ADR-0041). `copy` / `copy all` copy the whole panel; `copy last` copies the most recent command's output. diff --git a/src/app.rs b/src/app.rs index 54f42ec..425b91d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -605,6 +605,15 @@ impl App { self.handle_dsl_explain_success(&command, &plan); Vec::new() } + AppEvent::DslShowListSucceeded { command, lines } => { + // Mark the echo ✓ (ADR-0040), then emit the + // worker-formatted list as system output lines. + self.note_ok_summary(&command); + for line in lines { + self.note_system(line); + } + Vec::new() + } AppEvent::DslInsertSucceeded { command, result } => { self.handle_dsl_insert_success(&command, &result); Vec::new() @@ -2007,6 +2016,9 @@ impl App { C::ShowData { name, .. } | C::ShowTable { name } => { (Operation::Query, Some(name.as_str()), None) } + // A `show ` list spans no single table; a failure + // routes through Query with no table fallback. + C::ShowList { .. } => (Operation::Query, None, None), // A SQL `SELECT` carries only its statement text — // no single table name to fall back on. A query // failure routes through `Operation::Query`. @@ -2822,13 +2834,21 @@ mod tests { #[test] fn tab_cycles_forward_through_multi_candidate_set() { + // `show ` offers five subcommands in grammar order: + // data / table / tables / relationships / indexes (V5). let mut app = App::new(); type_str(&mut app, "show "); - app.update(key(KeyCode::Tab)); - assert_eq!(app.input, "show data"); - app.update(key(KeyCode::Tab)); - assert_eq!(app.input, "show table"); - // Wrap-around. + for expected in [ + "show data", + "show table", + "show tables", + "show relationships", + "show indexes", + ] { + app.update(key(KeyCode::Tab)); + assert_eq!(app.input, expected); + } + // Wrap-around back to the first. app.update(key(KeyCode::Tab)); assert_eq!(app.input, "show data"); } @@ -2837,12 +2857,11 @@ mod tests { fn shift_tab_cycles_backward_starting_from_last() { let mut app = App::new(); type_str(&mut app, "show "); - app.update(key(KeyCode::BackTab)); - assert_eq!(app.input, "show table"); - app.update(key(KeyCode::BackTab)); - assert_eq!(app.input, "show data"); - app.update(key(KeyCode::BackTab)); - assert_eq!(app.input, "show table"); + // Backward starts from the last candidate (`indexes`). + for expected in ["show indexes", "show relationships", "show tables"] { + app.update(key(KeyCode::BackTab)); + assert_eq!(app.input, expected); + } } #[test] diff --git a/src/completion.rs b/src/completion.rs index 10c9132..6884f28 100644 --- a/src/completion.rs +++ b/src/completion.rs @@ -1661,9 +1661,21 @@ mod tests { } #[test] - fn show_offers_data_and_table_alphabetised() { + fn show_offers_all_subcommands() { + // `show` branches: data / table (singular) plus the V5 + // list-all forms tables / relationships / indexes, in + // grammar-declaration order. let cs = cands("show ", 5); - assert_eq!(cs, vec!["data".to_string(), "table".to_string()]); + assert_eq!( + cs, + vec![ + "data".to_string(), + "table".to_string(), + "tables".to_string(), + "relationships".to_string(), + "indexes".to_string(), + ], + ); } #[test] diff --git a/src/db.rs b/src/db.rs index 661e5f6..7ec59d0 100644 --- a/src/db.rs +++ b/src/db.rs @@ -554,6 +554,14 @@ enum Request { ListTables { reply: oneshot::Sender, DbError>>, }, + /// List every item of a schema kind (tables / relationships / + /// indexes) as pre-formatted display lines for the `show + /// ` commands (V5). Read-only; formats in the worker + /// from the same helpers the items panel and describe view use. + ShowList { + kind: crate::dsl::command::ShowListKind, + reply: oneshot::Sender, DbError>>, + }, DescribeTable { name: String, source: Option, @@ -1317,6 +1325,17 @@ impl Database { recv.await.map_err(|_| DbError::WorkerGone)? } + /// Pre-formatted display lines for `show tables` / + /// `show relationships` / `show indexes` (V5). Read-only. + pub async fn show_list( + &self, + kind: crate::dsl::command::ShowListKind, + ) -> Result, DbError> { + let (reply, recv) = oneshot::channel(); + self.send(Request::ShowList { kind, reply }).await?; + recv.await.map_err(|_| DbError::WorkerGone)? + } + pub async fn describe_table( &self, name: String, @@ -2245,6 +2264,9 @@ fn handle_request( Request::ListTables { reply } => { let _ = reply.send(do_list_tables(conn)); } + Request::ShowList { kind, reply } => { + let _ = reply.send(do_show_list(conn, kind)); + } Request::DescribeTable { name, source, @@ -5834,6 +5856,79 @@ fn do_list_tables(conn: &Connection) -> Result, DbError> { Ok(out) } +/// Pre-formatted display lines for the `show ` list commands +/// (V5). A count header followed by one indented item per line, or a +/// single friendly "none yet" line for an empty collection. Reuses +/// the same helpers the items panel / describe view read from, so the +/// list never drifts from those views. Engine-neutral wording per the +/// ADR-0002 user-facing posture. +fn do_show_list( + conn: &Connection, + kind: crate::dsl::command::ShowListKind, +) -> Result, DbError> { + use crate::dsl::command::ShowListKind; + let mut lines = Vec::new(); + match kind { + ShowListKind::Tables => { + let tables = do_list_tables(conn)?; + if tables.is_empty() { + lines.push("No tables in this project yet.".to_string()); + } else { + lines.push(format!("Tables ({}):", tables.len())); + for name in tables { + lines.push(format!(" {name}")); + } + } + } + ShowListKind::Relationships => { + let rels = read_all_relationships(conn)?; + if rels.is_empty() { + lines.push("No relationships in this project yet.".to_string()); + } else { + lines.push(format!("Relationships ({}):", rels.len())); + for r in rels { + let mut line = format!( + " {}: {}.{} → {}.{}", + r.name, r.parent_table, r.parent_column, r.child_table, r.child_column + ); + if r.on_delete != ReferentialAction::default_action() { + line.push_str(&format!(" on delete {}", r.on_delete.keyword())); + } + if r.on_update != ReferentialAction::default_action() { + line.push_str(&format!(" on update {}", r.on_update.keyword())); + } + lines.push(line); + } + } + } + ShowListKind::Indexes => { + // Each table's user-created indexes (origin "c"), the + // same set the items panel shows. Ordered by table, then + // by index name (read_table_indexes orders by name). + let tables = do_list_tables(conn)?; + let mut entries: Vec = Vec::new(); + for table in &tables { + for ix in read_table_indexes(conn, table)? { + let unique = if ix.unique { " [unique]" } else { "" }; + entries.push(format!( + " {}.{} ({}){unique}", + table, + ix.name, + ix.columns.join(", ") + )); + } + } + if entries.is_empty() { + lines.push("No indexes in this project yet.".to_string()); + } else { + lines.push(format!("Indexes ({}):", entries.len())); + lines.extend(entries); + } + } + } + Ok(lines) +} + /// Internal full schema of a table, sufficient to regenerate /// its `CREATE TABLE` statement during the rebuild dance. #[derive(Debug, Clone)] diff --git a/src/dsl/command.rs b/src/dsl/command.rs index 805028a..60c8815 100644 --- a/src/dsl/command.rs +++ b/src/dsl/command.rs @@ -332,6 +332,13 @@ pub enum Command { ShowTable { name: String, }, + /// Re-display a whole schema collection — every table, + /// relationship, or index — as a list in the output (V5). The + /// read-only "all items" sibling of `ShowTable`; pure display, + /// no schema change. + ShowList { + kind: ShowListKind, + }, /// Insert a single row. `columns` is `None` for the natural- /// order short form (`insert into T values (...)`); the /// executor fills in the column list by walking the schema. @@ -746,6 +753,36 @@ pub enum IndexSelector { Columns { table: String, columns: Vec }, } +/// Which schema collection a `show ` list command displays (V5). +/// +/// The bare plural forms list every item of the kind across the +/// project; the singular `show table ` (a separate +/// `Command::ShowTable`) shows one. The singular `show +/// relationship ` / `show index ` forms are not yet +/// provided — only the list-all forms land here. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ShowListKind { + /// `show tables` — every user table (internal `__rdbms_*` + /// tables excluded, as in the items panel). + Tables, + /// `show relationships` — every declared FK relationship. + Relationships, + /// `show indexes` — every index across all tables. + Indexes, +} + +impl ShowListKind { + /// The full command name for the `name()` / echo surface. + #[must_use] + pub const fn command_name(self) -> &'static str { + match self { + Self::Tables => "show tables", + Self::Relationships => "show relationships", + Self::Indexes => "show indexes", + } + } +} + /// The action of an advanced-mode `ALTER TABLE` (ADR-0035 §4). /// /// Sub-phase 4e carries the column actions; 4f adds `AlterColumnType`; @@ -860,6 +897,7 @@ impl Command { Self::AddConstraint { .. } => "add constraint", Self::DropConstraint { .. } => "drop constraint", Self::ShowTable { .. } => "show table", + Self::ShowList { kind } => kind.command_name(), Self::Insert { .. } => "insert into", Self::Update { .. } => "update", Self::Delete { .. } => "delete from", @@ -948,6 +986,10 @@ impl Command { // result renders as a data view, not a structure // view, so an empty target is correct here. Self::Select { .. } => "", + // A `show ` list spans every table (or none) — + // there is no single structure-target table; it renders + // as a list, not a structure view. + Self::ShowList { .. } => "", // A SQL `INSERT` carries its parsed target table (for // CSV re-persistence and ok-summary subject). Self::SqlInsert { target_table, .. } diff --git a/src/dsl/grammar/data.rs b/src/dsl/grammar/data.rs index 81b901a..e57c3f1 100644 --- a/src/dsl/grammar/data.rs +++ b/src/dsl/grammar/data.rs @@ -24,7 +24,7 @@ //! later swap that capture for the same typed slots used here, adding //! live hints/highlighting. -use crate::dsl::command::{Command, Expr, RowFilter}; +use crate::dsl::command::{Command, Expr, RowFilter, ShowListKind}; use crate::dsl::grammar::{ CommandNode, IdentSource, Node, NumberValidator, ValidationError, Word, expr, shared::{ @@ -99,7 +99,21 @@ const SHOW_TABLE_NODES: &[Node] = &[ ]; const SHOW_TABLE: Node = Node::Seq(SHOW_TABLE_NODES); -const SHOW_CHOICES: &[Node] = &[SHOW_DATA, SHOW_TABLE]; +// `show tables` / `show relationships` / `show indexes` — the +// list-all forms (V5). Each is a single keyword with no argument; +// the executor lists every item of the kind. Distinct keyword +// tokens (`tables` ≠ `table`), so Choice ordering is irrelevant. +const SHOW_TABLES: Node = Node::Word(Word::keyword("tables")); +const SHOW_RELATIONSHIPS: Node = Node::Word(Word::keyword("relationships")); +const SHOW_INDEXES: Node = Node::Word(Word::keyword("indexes")); + +const SHOW_CHOICES: &[Node] = &[ + SHOW_DATA, + SHOW_TABLE, + SHOW_TABLES, + SHOW_RELATIONSHIPS, + SHOW_INDEXES, +]; const SHOW_SHAPE: Node = Node::Choice(SHOW_CHOICES); // ================================================================= @@ -552,10 +566,23 @@ fn build_show(path: &MatchedPath, _source: &str) -> Result None, }) .nth(1); - let name = require_ident(path, "table_name")?; match sub { Some("data") => build_show_data(path, _source), - Some("table") => Ok(Command::ShowTable { name }), + // `name` is resolved only for the forms that carry one; the + // list-all forms (`tables` / `relationships` / `indexes`) + // have no table argument. + Some("table") => Ok(Command::ShowTable { + name: require_ident(path, "table_name")?, + }), + Some("tables") => Ok(Command::ShowList { + kind: ShowListKind::Tables, + }), + Some("relationships") => Ok(Command::ShowList { + kind: ShowListKind::Relationships, + }), + Some("indexes") => Ok(Command::ShowList { + kind: ShowListKind::Indexes, + }), _ => Err(ValidationError { message_key: "parse.error_wrapper", args: vec![("detail", "unknown show subcommand".to_string())], @@ -1362,7 +1389,13 @@ pub static SHOW: CommandNode = CommandNode { shape: SHOW_SHAPE, ast_builder: build_show, help_id: Some("data.show"), - usage_ids: &["parse.usage.show_data", "parse.usage.show_table"],}; + usage_ids: &[ + "parse.usage.show_data", + "parse.usage.show_table", + "parse.usage.show_tables", + "parse.usage.show_relationships", + "parse.usage.show_indexes", + ],}; pub static INSERT: CommandNode = CommandNode { entry: Word::keyword("insert"), diff --git a/src/dsl/mod.rs b/src/dsl/mod.rs index 16da2ed..0c6b795 100644 --- a/src/dsl/mod.rs +++ b/src/dsl/mod.rs @@ -23,7 +23,7 @@ pub use action::ReferentialAction; pub use command::{ AlterTableAction, AppCommand, ChangeColumnMode, ColumnSpec, Command, CompareOp, CopyScope, Expr, IndexSelector, MessagesValue, ModeValue, Operand, Predicate, RelationshipSelector, RowFilter, - SqlForeignKey, + ShowListKind, SqlForeignKey, }; pub use parser::{ParseError, parse_command}; pub use types::Type; diff --git a/src/event.rs b/src/event.rs index 9266a09..6bde025 100644 --- a/src/event.rs +++ b/src/event.rs @@ -73,6 +73,9 @@ pub enum AppEvent { /// An `explain …` command succeeded (ADR-0028). `plan` /// carries the captured query plan; nothing was executed. DslExplainSucceeded { command: Command, plan: QueryPlan }, + /// A `show ` list command (V5) — carries pre-formatted + /// display lines (tables / relationships / indexes). + DslShowListSucceeded { command: Command, lines: Vec }, DslInsertSucceeded { command: Command, result: InsertResult, diff --git a/src/friendly/keys.rs b/src/friendly/keys.rs index 3c4d8fd..13c9b9f 100644 --- a/src/friendly/keys.rs +++ b/src/friendly/keys.rs @@ -306,6 +306,9 @@ pub const KEYS_AND_PLACEHOLDERS: &[(&str, &[&str])] = &[ ("parse.usage.select", &[]), ("parse.usage.show_data", &[]), ("parse.usage.show_table", &[]), + ("parse.usage.show_tables", &[]), + ("parse.usage.show_relationships", &[]), + ("parse.usage.show_indexes", &[]), ("parse.usage.update", &[]), ("parse.usage.with", &[]), ("parse.expect.select_projection", &[]), diff --git a/src/friendly/strings/en-US.yaml b/src/friendly/strings/en-US.yaml index 12fc0e3..9366f7f 100644 --- a/src/friendly/strings/en-US.yaml +++ b/src/friendly/strings/en-US.yaml @@ -317,6 +317,9 @@ help: show: |- show table — show a table's structure show data — show a table's rows + show tables — list all tables + show relationships — list all relationships + show indexes — list all indexes insert: |- insert into [(cols)] [values] (vals) — add a row update: |- @@ -554,6 +557,9 @@ parse: [--force-conversion | --dont-convert] show_data: "show data
" show_table: "show table
" + show_tables: "show tables" + show_relationships: "show relationships" + show_indexes: "show indexes" insert: "insert into
[([, ...])] [values] ([, ...])" update: "update
set =[, ...] (where = | --all-rows)" delete: "delete from
(where = | --all-rows)" diff --git a/src/input_render.rs b/src/input_render.rs index ec19458..95f7c24 100644 --- a/src/input_render.rs +++ b/src/input_render.rs @@ -2050,9 +2050,19 @@ mod tests { } #[test] - fn ambient_hint_at_word_boundary_after_show_returns_data_table() { + fn ambient_hint_at_word_boundary_after_show_returns_all_subcommands() { + // data / table plus the V5 list-all forms, grammar order. let cs = cands_hint("show ", 5).expect("candidate hint"); - assert_eq!(cs, vec!["data".to_string(), "table".to_string()]); + assert_eq!( + cs, + vec![ + "data".to_string(), + "table".to_string(), + "tables".to_string(), + "relationships".to_string(), + "indexes".to_string(), + ], + ); } #[test] diff --git a/src/runtime.rs b/src/runtime.rs index d29b874..c94ca11 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -1403,6 +1403,10 @@ fn spawn_dsl_dispatch( echo, } } + Ok(CommandOutcome::ShowList(lines)) => AppEvent::DslShowListSucceeded { + command: command.clone(), + lines, + }, Ok(CommandOutcome::QueryPlan(plan)) => AppEvent::DslExplainSucceeded { command: command.clone(), plan, @@ -2244,6 +2248,10 @@ enum CommandOutcome { /// — skipped" note. SchemaCreateIndexSkipped(String), Query(DataResult), + /// A `show ` list (V5) — pre-formatted display lines from + /// the worker (table / relationship / index names). Pure + /// display, no schema change. + ShowList(Vec), QueryPlan(QueryPlan), Insert(InsertResult), Update(UpdateResult), @@ -2766,6 +2774,9 @@ async fn execute_command_typed( .describe_table(name, src) .await .map(|d| CommandOutcome::Schema(Some(d))), + Command::ShowList { kind } => { + database.show_list(kind).await.map(CommandOutcome::ShowList) + } Command::Insert { table, columns, diff --git a/tests/it/main.rs b/tests/it/main.rs index 3bd4270..754daa2 100644 --- a/tests/it/main.rs +++ b/tests/it/main.rs @@ -30,5 +30,6 @@ mod sql_drop_table; mod sql_insert; mod sql_select; mod sql_update; +mod show_list; mod undo_snapshots; mod walking_skeleton; diff --git a/tests/it/show_list.rs b/tests/it/show_list.rs new file mode 100644 index 0000000..4150e1c --- /dev/null +++ b/tests/it/show_list.rs @@ -0,0 +1,276 @@ +//! Integration tests for the `show ` list commands (V5): +//! `show tables`, `show relationships`, `show indexes`. +//! +//! Covers: +//! - Parse layer: each form parses to `Command::ShowList { kind }` +//! in both simple and advanced mode (the forms are +//! `CommandCategory::Simple`, available in both). +//! - Worker round-trip: `Database::show_list` returns the correct +//! pre-formatted lines after real DDL (tables, a relationship, +//! an index), and the empty-collection wording. +//! - App end-to-end: submitting `show tables` reaches the output +//! panel as system lines and marks the echo complete. + +use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; + +use rdbms_playground::action::Action; +use rdbms_playground::app::App; +use rdbms_playground::db::Database; +use rdbms_playground::dsl::{ + parse_command, ColumnSpec, Command, ReferentialAction, ShowListKind, Type, +}; +use rdbms_playground::event::AppEvent; +use rdbms_playground::mode::Mode; +use rdbms_playground::persistence::Persistence; +use rdbms_playground::project; + +// ================================================================= +// Parse layer +// ================================================================= + +#[test] +fn show_tables_parses_as_show_list_tables() { + assert_eq!( + parse_command("show tables").expect("parses"), + Command::ShowList { + kind: ShowListKind::Tables + }, + ); +} + +#[test] +fn show_relationships_parses_as_show_list_relationships() { + assert_eq!( + parse_command("show relationships").expect("parses"), + Command::ShowList { + kind: ShowListKind::Relationships + }, + ); +} + +#[test] +fn show_indexes_parses_as_show_list_indexes() { + assert_eq!( + parse_command("show indexes").expect("parses"), + Command::ShowList { + kind: ShowListKind::Indexes + }, + ); +} + +#[test] +fn show_table_singular_still_parses_as_show_table() { + // The new plural keyword must not shadow the singular + // `show table ` form — `table` ≠ `tables`. + assert_eq!( + parse_command("show table Customers").expect("parses"), + Command::ShowTable { + name: "Customers".to_string() + }, + ); +} + +// ================================================================= +// Worker round-trip — real execution +// ================================================================= + +fn rt() -> tokio::runtime::Runtime { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("tokio rt") +} + +fn open_project_db() -> (project::Project, Database, tempfile::TempDir) { + let dir = tempfile::tempdir().expect("create tempdir"); + let project = + project::open_or_create(None, Some(dir.path())).expect("open or create project"); + let persistence = Persistence::new(project.path().to_path_buf()); + let db = Database::open_with_persistence(project.db_path(), persistence) + .expect("open db with persistence"); + (project, db, dir) +} + +/// Create two related tables plus an index, so each list kind has +/// content. Returns once the worker has applied everything. +async fn seed_schema(db: &Database) { + db.create_table( + "Customers".to_string(), + vec![ + ColumnSpec::new("id", Type::Serial), + ColumnSpec::new("Name", Type::Text), + ], + vec!["id".to_string()], + None, + ) + .await + .expect("create Customers"); + db.create_table( + "Orders".to_string(), + vec![ + ColumnSpec::new("id", Type::Serial), + ColumnSpec::new("customer_id", Type::Int), + ], + vec!["id".to_string()], + None, + ) + .await + .expect("create Orders"); + db.add_relationship( + Some("orders_customer".to_string()), + "Customers".to_string(), + "id".to_string(), + "Orders".to_string(), + "customer_id".to_string(), + ReferentialAction::Cascade, + ReferentialAction::NoAction, + false, + None, + ) + .await + .expect("add relationship"); + db.add_index( + Some("idx_orders_customer".to_string()), + "Orders".to_string(), + vec!["customer_id".to_string()], + None, + ) + .await + .expect("add index"); +} + +#[test] +fn show_tables_lists_all_user_tables_with_count_header() { + let (_p, db, _dir) = open_project_db(); + let rt = rt(); + rt.block_on(seed_schema(&db)); + let lines = rt + .block_on(db.show_list(ShowListKind::Tables)) + .expect("show tables"); + assert_eq!(lines[0], "Tables (2):", "count header"); + assert!( + lines.iter().any(|l| l == " Customers"), + "Customers listed: {lines:?}", + ); + assert!( + lines.iter().any(|l| l == " Orders"), + "Orders listed: {lines:?}", + ); +} + +#[test] +fn show_relationships_lists_name_endpoints_and_nondefault_action() { + let (_p, db, _dir) = open_project_db(); + let rt = rt(); + rt.block_on(seed_schema(&db)); + let lines = rt + .block_on(db.show_list(ShowListKind::Relationships)) + .expect("show relationships"); + assert_eq!(lines[0], "Relationships (1):"); + // Name, both endpoints, and the non-default ON DELETE CASCADE + // (ON UPDATE NO ACTION is the default and is omitted). + assert_eq!( + lines[1], + " orders_customer: Customers.id → Orders.customer_id on delete cascade", + "relationship summary line: {lines:?}", + ); +} + +#[test] +fn show_indexes_lists_qualified_name_and_columns() { + let (_p, db, _dir) = open_project_db(); + let rt = rt(); + rt.block_on(seed_schema(&db)); + let lines = rt + .block_on(db.show_list(ShowListKind::Indexes)) + .expect("show indexes"); + assert_eq!(lines[0], "Indexes (1):"); + assert_eq!( + lines[1], " Orders.idx_orders_customer (customer_id)", + "index summary line: {lines:?}", + ); +} + +#[test] +fn show_lists_report_empty_collections_with_friendly_lines() { + let (_p, db, _dir) = open_project_db(); + let rt = rt(); + // No schema seeded — every kind is empty. + assert_eq!( + rt.block_on(db.show_list(ShowListKind::Tables)).unwrap(), + vec!["No tables in this project yet.".to_string()], + ); + assert_eq!( + rt.block_on(db.show_list(ShowListKind::Relationships)) + .unwrap(), + vec!["No relationships in this project yet.".to_string()], + ); + assert_eq!( + rt.block_on(db.show_list(ShowListKind::Indexes)).unwrap(), + vec!["No indexes in this project yet.".to_string()], + ); +} + +// ================================================================= +// App end-to-end +// ================================================================= + +const fn key(code: KeyCode) -> AppEvent { + AppEvent::Key(KeyEvent { + code, + modifiers: KeyModifiers::NONE, + kind: KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE, + }) +} + +fn type_str(app: &mut App, s: &str) { + for c in s.chars() { + app.update(key(KeyCode::Char(c))); + } +} + +#[test] +fn app_show_tables_dispatches_show_list_command() { + let mut app = App::new(); + app.mode = Mode::Simple; + type_str(&mut app, "show tables"); + let actions = app.update(key(KeyCode::Enter)); + let dispatched = actions.iter().any(|a| { + matches!( + a, + Action::ExecuteDsl { + command: Command::ShowList { + kind: ShowListKind::Tables + }, + .. + } + ) + }); + assert!(dispatched, "submit dispatches ShowList(Tables): {actions:?}"); +} + +#[test] +fn app_renders_show_list_lines_as_system_output() { + // Feed the success event directly so the test stays + // self-contained (the worker round-trip is covered above). + let mut app = App::new(); + app.output.push_back(rdbms_playground::app::OutputLine::echo( + "show tables", + Mode::Simple, + )); + app.update(AppEvent::DslShowListSucceeded { + command: Command::ShowList { + kind: ShowListKind::Tables, + }, + lines: vec!["Tables (1):".to_string(), " Customers".to_string()], + }); + assert!( + app.output.iter().any(|l| l.text == "Tables (1):"), + "header line rendered", + ); + assert!( + app.output.iter().any(|l| l.text == " Customers"), + "item line rendered", + ); +} diff --git a/tests/typing_surface/mod.rs b/tests/typing_surface/mod.rs index 51e94b5..55a57d2 100644 --- a/tests/typing_surface/mod.rs +++ b/tests/typing_surface/mod.rs @@ -233,6 +233,7 @@ fn command_kind_label(cmd: &rdbms_playground::dsl::Command) -> String { AddConstraint { .. } => "AddConstraint".into(), DropConstraint { .. } => "DropConstraint".into(), ShowTable { .. } => "ShowTable".into(), + ShowList { kind } => format!("ShowList({kind:?})"), Insert { .. } => "Insert".into(), Update { .. } => "Update".into(), Delete { .. } => "Delete".into(),