//! 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, name: None, }, ); } #[test] fn show_relationships_parses_as_show_list_relationships() { assert_eq!( parse_command("show relationships").expect("parses"), Command::ShowList { kind: ShowListKind::Relationships, name: None, }, ); } #[test] fn show_indexes_parses_as_show_list_indexes() { assert_eq!( parse_command("show indexes").expect("parses"), Command::ShowList { kind: ShowListKind::Indexes, name: None, }, ); } #[test] fn show_relationship_singular_parses_with_name() { assert_eq!( parse_command("show relationship orders_customer").expect("parses"), Command::ShowList { kind: ShowListKind::Relationships, name: Some("orders_customer".to_string()), }, ); } #[test] fn show_index_singular_parses_with_name() { assert_eq!( parse_command("show index idx_orders_customer").expect("parses"), Command::ShowList { kind: ShowListKind::Indexes, name: Some("idx_orders_customer".to_string()), }, ); } #[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(), vec!["id".to_string()], "Orders".to_string(), vec!["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, None)) .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, None)) .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, None)) .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, None)).unwrap(), vec!["No tables in this project yet.".to_string()], ); assert_eq!( rt.block_on(db.show_list(ShowListKind::Relationships, None)) .unwrap(), vec!["No relationships in this project yet.".to_string()], ); assert_eq!( rt.block_on(db.show_list(ShowListKind::Indexes, None)).unwrap(), vec!["No indexes in this project yet.".to_string()], ); } // ================================================================= // V5a — singular per-item detail // ================================================================= #[test] fn show_one_relationship_renders_detail_block() { 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, Some("orders_customer".to_string()))) .expect("show relationship"); assert_eq!(lines[0], "Relationship `orders_customer`:"); assert_eq!(lines[1], " Customers.id → Orders.customer_id"); assert!( lines.iter().any(|l| l == " on delete cascade"), "on-delete shown: {lines:?}", ); } #[test] fn show_one_index_renders_detail_block() { 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, Some("idx_orders_customer".to_string()))) .expect("show index"); assert_eq!(lines[0], "Index `idx_orders_customer` on Orders:"); assert!( lines.iter().any(|l| l == " columns: customer_id"), "columns shown: {lines:?}", ); assert!( lines.iter().any(|l| l == " unique: no"), "uniqueness shown: {lines:?}", ); } #[test] fn show_one_unknown_name_reports_friendly_not_found() { let (_p, db, _dir) = open_project_db(); let rt = rt(); rt.block_on(seed_schema(&db)); assert_eq!( rt.block_on(db.show_list(ShowListKind::Relationships, Some("nope".to_string()))) .unwrap(), vec!["No relationship named `nope`.".to_string()], ); assert_eq!( rt.block_on(db.show_list(ShowListKind::Indexes, Some("nope".to_string()))) .unwrap(), vec!["No index named `nope`.".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, name: None, }, .. } ) }); 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, name: None, }, 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", ); }