feat: V5 show tables / relationships / indexes list commands
Add the list-all show family as one Command::ShowList { kind }
variant. A read-only worker show_list formats count-headed lists
(reusing do_list_tables / read_all_relationships /
read_table_indexes, so it never drifts from the items panel);
internal __rdbms_* tables excluded. Help + parse-usage entries
added; 10 integration tests in tests/it/show_list.rs.
Mark V5 [x]. Split the singular show relationship/index <name>
detail forms (the [<name>] half) into a new tracked V5a [ ] item
rather than leaving them as an untracked footnote.
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
//! Integration tests for the `show <kind>` 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 <name>` 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",
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user