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:
+22
-6
@@ -450,13 +450,29 @@ since ADR-0027.)
|
|||||||
buffer is in, with new output snapping the view to the most
|
buffer is in, with new output snapping the view to the most
|
||||||
recent. The full V4 scope — smart structure rendering, log
|
recent. The full V4 scope — smart structure rendering, log
|
||||||
styling, Markdown export, scroll indicator — remains pending.)*
|
styling, Markdown export, scroll indicator — remains pending.)*
|
||||||
- [/] **V5** `show <kind> [<name>]` family of commands for
|
- [x] **V5** `show <kind> [<name>]` family of commands for
|
||||||
redisplaying schema info on demand.
|
redisplaying schema info on demand.
|
||||||
*(Partial, verified 2026-06-07: `show table <name>` and
|
*(Done 2026-06-07: `show table <name>` + `show data <Table>`
|
||||||
`show data <Table>` implemented (`grammar/data.rs`). **Missing
|
(single-item) plus the list-all family `show tables` /
|
||||||
the "all items" variants** — `show tables`, `show
|
`show relationships` / `show indexes` — the latter three landed
|
||||||
relationships`, `show indexes` — none are registered. V5 closes
|
as `Command::ShowList { kind }` (one variant, `grammar/data.rs`),
|
||||||
when the plural/list forms land.)*
|
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 `[<name>]` 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
|
||||||
|
<name>` / `show index <name>` — the `[<name>]` half of V5 for
|
||||||
|
the relationship and index kinds (the table kind already has
|
||||||
|
`show table <name>`). 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
|
- [x] **V6** Copy the output panel to the system clipboard
|
||||||
(issue #11, ADR-0041). `copy` / `copy all` copy the whole
|
(issue #11, ADR-0041). `copy` / `copy all` copy the whole
|
||||||
panel; `copy last` copies the most recent command's output.
|
panel; `copy last` copies the most recent command's output.
|
||||||
|
|||||||
+30
-11
@@ -605,6 +605,15 @@ impl App {
|
|||||||
self.handle_dsl_explain_success(&command, &plan);
|
self.handle_dsl_explain_success(&command, &plan);
|
||||||
Vec::new()
|
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 } => {
|
AppEvent::DslInsertSucceeded { command, result } => {
|
||||||
self.handle_dsl_insert_success(&command, &result);
|
self.handle_dsl_insert_success(&command, &result);
|
||||||
Vec::new()
|
Vec::new()
|
||||||
@@ -2007,6 +2016,9 @@ impl App {
|
|||||||
C::ShowData { name, .. } | C::ShowTable { name } => {
|
C::ShowData { name, .. } | C::ShowTable { name } => {
|
||||||
(Operation::Query, Some(name.as_str()), None)
|
(Operation::Query, Some(name.as_str()), None)
|
||||||
}
|
}
|
||||||
|
// A `show <kind>` 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 —
|
// A SQL `SELECT` carries only its statement text —
|
||||||
// no single table name to fall back on. A query
|
// no single table name to fall back on. A query
|
||||||
// failure routes through `Operation::Query`.
|
// failure routes through `Operation::Query`.
|
||||||
@@ -2822,13 +2834,21 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tab_cycles_forward_through_multi_candidate_set() {
|
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();
|
let mut app = App::new();
|
||||||
type_str(&mut app, "show ");
|
type_str(&mut app, "show ");
|
||||||
app.update(key(KeyCode::Tab));
|
for expected in [
|
||||||
assert_eq!(app.input, "show data");
|
"show data",
|
||||||
app.update(key(KeyCode::Tab));
|
"show table",
|
||||||
assert_eq!(app.input, "show table");
|
"show tables",
|
||||||
// Wrap-around.
|
"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));
|
app.update(key(KeyCode::Tab));
|
||||||
assert_eq!(app.input, "show data");
|
assert_eq!(app.input, "show data");
|
||||||
}
|
}
|
||||||
@@ -2837,12 +2857,11 @@ mod tests {
|
|||||||
fn shift_tab_cycles_backward_starting_from_last() {
|
fn shift_tab_cycles_backward_starting_from_last() {
|
||||||
let mut app = App::new();
|
let mut app = App::new();
|
||||||
type_str(&mut app, "show ");
|
type_str(&mut app, "show ");
|
||||||
app.update(key(KeyCode::BackTab));
|
// Backward starts from the last candidate (`indexes`).
|
||||||
assert_eq!(app.input, "show table");
|
for expected in ["show indexes", "show relationships", "show tables"] {
|
||||||
app.update(key(KeyCode::BackTab));
|
app.update(key(KeyCode::BackTab));
|
||||||
assert_eq!(app.input, "show data");
|
assert_eq!(app.input, expected);
|
||||||
app.update(key(KeyCode::BackTab));
|
}
|
||||||
assert_eq!(app.input, "show table");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
+14
-2
@@ -1661,9 +1661,21 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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);
|
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]
|
#[test]
|
||||||
|
|||||||
@@ -554,6 +554,14 @@ enum Request {
|
|||||||
ListTables {
|
ListTables {
|
||||||
reply: oneshot::Sender<Result<Vec<String>, DbError>>,
|
reply: oneshot::Sender<Result<Vec<String>, DbError>>,
|
||||||
},
|
},
|
||||||
|
/// List every item of a schema kind (tables / relationships /
|
||||||
|
/// indexes) as pre-formatted display lines for the `show
|
||||||
|
/// <kind>` 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<Result<Vec<String>, DbError>>,
|
||||||
|
},
|
||||||
DescribeTable {
|
DescribeTable {
|
||||||
name: String,
|
name: String,
|
||||||
source: Option<String>,
|
source: Option<String>,
|
||||||
@@ -1317,6 +1325,17 @@ impl Database {
|
|||||||
recv.await.map_err(|_| DbError::WorkerGone)?
|
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<Vec<String>, DbError> {
|
||||||
|
let (reply, recv) = oneshot::channel();
|
||||||
|
self.send(Request::ShowList { kind, reply }).await?;
|
||||||
|
recv.await.map_err(|_| DbError::WorkerGone)?
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn describe_table(
|
pub async fn describe_table(
|
||||||
&self,
|
&self,
|
||||||
name: String,
|
name: String,
|
||||||
@@ -2245,6 +2264,9 @@ fn handle_request(
|
|||||||
Request::ListTables { reply } => {
|
Request::ListTables { reply } => {
|
||||||
let _ = reply.send(do_list_tables(conn));
|
let _ = reply.send(do_list_tables(conn));
|
||||||
}
|
}
|
||||||
|
Request::ShowList { kind, reply } => {
|
||||||
|
let _ = reply.send(do_show_list(conn, kind));
|
||||||
|
}
|
||||||
Request::DescribeTable {
|
Request::DescribeTable {
|
||||||
name,
|
name,
|
||||||
source,
|
source,
|
||||||
@@ -5834,6 +5856,79 @@ fn do_list_tables(conn: &Connection) -> Result<Vec<String>, DbError> {
|
|||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Pre-formatted display lines for the `show <kind>` 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<Vec<String>, 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<String> = 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
|
/// Internal full schema of a table, sufficient to regenerate
|
||||||
/// its `CREATE TABLE` statement during the rebuild dance.
|
/// its `CREATE TABLE` statement during the rebuild dance.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|||||||
@@ -332,6 +332,13 @@ pub enum Command {
|
|||||||
ShowTable {
|
ShowTable {
|
||||||
name: String,
|
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-
|
/// Insert a single row. `columns` is `None` for the natural-
|
||||||
/// order short form (`insert into T values (...)`); the
|
/// order short form (`insert into T values (...)`); the
|
||||||
/// executor fills in the column list by walking the schema.
|
/// executor fills in the column list by walking the schema.
|
||||||
@@ -746,6 +753,36 @@ pub enum IndexSelector {
|
|||||||
Columns { table: String, columns: Vec<String> },
|
Columns { table: String, columns: Vec<String> },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Which schema collection a `show <kind>` list command displays (V5).
|
||||||
|
///
|
||||||
|
/// The bare plural forms list every item of the kind across the
|
||||||
|
/// project; the singular `show table <name>` (a separate
|
||||||
|
/// `Command::ShowTable`) shows one. The singular `show
|
||||||
|
/// relationship <name>` / `show index <name>` 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).
|
/// The action of an advanced-mode `ALTER TABLE` (ADR-0035 §4).
|
||||||
///
|
///
|
||||||
/// Sub-phase 4e carries the column actions; 4f adds `AlterColumnType`;
|
/// Sub-phase 4e carries the column actions; 4f adds `AlterColumnType`;
|
||||||
@@ -860,6 +897,7 @@ impl Command {
|
|||||||
Self::AddConstraint { .. } => "add constraint",
|
Self::AddConstraint { .. } => "add constraint",
|
||||||
Self::DropConstraint { .. } => "drop constraint",
|
Self::DropConstraint { .. } => "drop constraint",
|
||||||
Self::ShowTable { .. } => "show table",
|
Self::ShowTable { .. } => "show table",
|
||||||
|
Self::ShowList { kind } => kind.command_name(),
|
||||||
Self::Insert { .. } => "insert into",
|
Self::Insert { .. } => "insert into",
|
||||||
Self::Update { .. } => "update",
|
Self::Update { .. } => "update",
|
||||||
Self::Delete { .. } => "delete from",
|
Self::Delete { .. } => "delete from",
|
||||||
@@ -948,6 +986,10 @@ impl Command {
|
|||||||
// result renders as a data view, not a structure
|
// result renders as a data view, not a structure
|
||||||
// view, so an empty target is correct here.
|
// view, so an empty target is correct here.
|
||||||
Self::Select { .. } => "",
|
Self::Select { .. } => "",
|
||||||
|
// A `show <kind>` 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
|
// A SQL `INSERT` carries its parsed target table (for
|
||||||
// CSV re-persistence and ok-summary subject).
|
// CSV re-persistence and ok-summary subject).
|
||||||
Self::SqlInsert { target_table, .. }
|
Self::SqlInsert { target_table, .. }
|
||||||
|
|||||||
+38
-5
@@ -24,7 +24,7 @@
|
|||||||
//! later swap that capture for the same typed slots used here, adding
|
//! later swap that capture for the same typed slots used here, adding
|
||||||
//! live hints/highlighting.
|
//! live hints/highlighting.
|
||||||
|
|
||||||
use crate::dsl::command::{Command, Expr, RowFilter};
|
use crate::dsl::command::{Command, Expr, RowFilter, ShowListKind};
|
||||||
use crate::dsl::grammar::{
|
use crate::dsl::grammar::{
|
||||||
CommandNode, IdentSource, Node, NumberValidator, ValidationError, Word, expr,
|
CommandNode, IdentSource, Node, NumberValidator, ValidationError, Word, expr,
|
||||||
shared::{
|
shared::{
|
||||||
@@ -99,7 +99,21 @@ const SHOW_TABLE_NODES: &[Node] = &[
|
|||||||
];
|
];
|
||||||
const SHOW_TABLE: Node = Node::Seq(SHOW_TABLE_NODES);
|
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);
|
const SHOW_SHAPE: Node = Node::Choice(SHOW_CHOICES);
|
||||||
|
|
||||||
// =================================================================
|
// =================================================================
|
||||||
@@ -552,10 +566,23 @@ fn build_show(path: &MatchedPath, _source: &str) -> Result<Command, ValidationEr
|
|||||||
_ => None,
|
_ => None,
|
||||||
})
|
})
|
||||||
.nth(1);
|
.nth(1);
|
||||||
let name = require_ident(path, "table_name")?;
|
|
||||||
match sub {
|
match sub {
|
||||||
Some("data") => build_show_data(path, _source),
|
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 {
|
_ => Err(ValidationError {
|
||||||
message_key: "parse.error_wrapper",
|
message_key: "parse.error_wrapper",
|
||||||
args: vec![("detail", "unknown show subcommand".to_string())],
|
args: vec![("detail", "unknown show subcommand".to_string())],
|
||||||
@@ -1362,7 +1389,13 @@ pub static SHOW: CommandNode = CommandNode {
|
|||||||
shape: SHOW_SHAPE,
|
shape: SHOW_SHAPE,
|
||||||
ast_builder: build_show,
|
ast_builder: build_show,
|
||||||
help_id: Some("data.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 {
|
pub static INSERT: CommandNode = CommandNode {
|
||||||
entry: Word::keyword("insert"),
|
entry: Word::keyword("insert"),
|
||||||
|
|||||||
+1
-1
@@ -23,7 +23,7 @@ pub use action::ReferentialAction;
|
|||||||
pub use command::{
|
pub use command::{
|
||||||
AlterTableAction, AppCommand, ChangeColumnMode, ColumnSpec, Command, CompareOp, CopyScope, Expr,
|
AlterTableAction, AppCommand, ChangeColumnMode, ColumnSpec, Command, CompareOp, CopyScope, Expr,
|
||||||
IndexSelector, MessagesValue, ModeValue, Operand, Predicate, RelationshipSelector, RowFilter,
|
IndexSelector, MessagesValue, ModeValue, Operand, Predicate, RelationshipSelector, RowFilter,
|
||||||
SqlForeignKey,
|
ShowListKind, SqlForeignKey,
|
||||||
};
|
};
|
||||||
pub use parser::{ParseError, parse_command};
|
pub use parser::{ParseError, parse_command};
|
||||||
pub use types::Type;
|
pub use types::Type;
|
||||||
|
|||||||
@@ -73,6 +73,9 @@ pub enum AppEvent {
|
|||||||
/// An `explain …` command succeeded (ADR-0028). `plan`
|
/// An `explain …` command succeeded (ADR-0028). `plan`
|
||||||
/// carries the captured query plan; nothing was executed.
|
/// carries the captured query plan; nothing was executed.
|
||||||
DslExplainSucceeded { command: Command, plan: QueryPlan },
|
DslExplainSucceeded { command: Command, plan: QueryPlan },
|
||||||
|
/// A `show <kind>` list command (V5) — carries pre-formatted
|
||||||
|
/// display lines (tables / relationships / indexes).
|
||||||
|
DslShowListSucceeded { command: Command, lines: Vec<String> },
|
||||||
DslInsertSucceeded {
|
DslInsertSucceeded {
|
||||||
command: Command,
|
command: Command,
|
||||||
result: InsertResult,
|
result: InsertResult,
|
||||||
|
|||||||
@@ -306,6 +306,9 @@ pub const KEYS_AND_PLACEHOLDERS: &[(&str, &[&str])] = &[
|
|||||||
("parse.usage.select", &[]),
|
("parse.usage.select", &[]),
|
||||||
("parse.usage.show_data", &[]),
|
("parse.usage.show_data", &[]),
|
||||||
("parse.usage.show_table", &[]),
|
("parse.usage.show_table", &[]),
|
||||||
|
("parse.usage.show_tables", &[]),
|
||||||
|
("parse.usage.show_relationships", &[]),
|
||||||
|
("parse.usage.show_indexes", &[]),
|
||||||
("parse.usage.update", &[]),
|
("parse.usage.update", &[]),
|
||||||
("parse.usage.with", &[]),
|
("parse.usage.with", &[]),
|
||||||
("parse.expect.select_projection", &[]),
|
("parse.expect.select_projection", &[]),
|
||||||
|
|||||||
@@ -317,6 +317,9 @@ help:
|
|||||||
show: |-
|
show: |-
|
||||||
show table <T> — show a table's structure
|
show table <T> — show a table's structure
|
||||||
show data <T> — show a table's rows
|
show data <T> — show a table's rows
|
||||||
|
show tables — list all tables
|
||||||
|
show relationships — list all relationships
|
||||||
|
show indexes — list all indexes
|
||||||
insert: |-
|
insert: |-
|
||||||
insert into <T> [(cols)] [values] (vals) — add a row
|
insert into <T> [(cols)] [values] (vals) — add a row
|
||||||
update: |-
|
update: |-
|
||||||
@@ -554,6 +557,9 @@ parse:
|
|||||||
[--force-conversion | --dont-convert]
|
[--force-conversion | --dont-convert]
|
||||||
show_data: "show data <Table>"
|
show_data: "show data <Table>"
|
||||||
show_table: "show table <Table>"
|
show_table: "show table <Table>"
|
||||||
|
show_tables: "show tables"
|
||||||
|
show_relationships: "show relationships"
|
||||||
|
show_indexes: "show indexes"
|
||||||
insert: "insert into <Table> [(<col>[, ...])] [values] (<value>[, ...])"
|
insert: "insert into <Table> [(<col>[, ...])] [values] (<value>[, ...])"
|
||||||
update: "update <Table> set <col>=<value>[, ...] (where <col>=<value> | --all-rows)"
|
update: "update <Table> set <col>=<value>[, ...] (where <col>=<value> | --all-rows)"
|
||||||
delete: "delete from <Table> (where <col>=<value> | --all-rows)"
|
delete: "delete from <Table> (where <col>=<value> | --all-rows)"
|
||||||
|
|||||||
+12
-2
@@ -2050,9 +2050,19 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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");
|
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]
|
#[test]
|
||||||
|
|||||||
@@ -1403,6 +1403,10 @@ fn spawn_dsl_dispatch(
|
|||||||
echo,
|
echo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Ok(CommandOutcome::ShowList(lines)) => AppEvent::DslShowListSucceeded {
|
||||||
|
command: command.clone(),
|
||||||
|
lines,
|
||||||
|
},
|
||||||
Ok(CommandOutcome::QueryPlan(plan)) => AppEvent::DslExplainSucceeded {
|
Ok(CommandOutcome::QueryPlan(plan)) => AppEvent::DslExplainSucceeded {
|
||||||
command: command.clone(),
|
command: command.clone(),
|
||||||
plan,
|
plan,
|
||||||
@@ -2244,6 +2248,10 @@ enum CommandOutcome {
|
|||||||
/// — skipped" note.
|
/// — skipped" note.
|
||||||
SchemaCreateIndexSkipped(String),
|
SchemaCreateIndexSkipped(String),
|
||||||
Query(DataResult),
|
Query(DataResult),
|
||||||
|
/// A `show <kind>` list (V5) — pre-formatted display lines from
|
||||||
|
/// the worker (table / relationship / index names). Pure
|
||||||
|
/// display, no schema change.
|
||||||
|
ShowList(Vec<String>),
|
||||||
QueryPlan(QueryPlan),
|
QueryPlan(QueryPlan),
|
||||||
Insert(InsertResult),
|
Insert(InsertResult),
|
||||||
Update(UpdateResult),
|
Update(UpdateResult),
|
||||||
@@ -2766,6 +2774,9 @@ async fn execute_command_typed(
|
|||||||
.describe_table(name, src)
|
.describe_table(name, src)
|
||||||
.await
|
.await
|
||||||
.map(|d| CommandOutcome::Schema(Some(d))),
|
.map(|d| CommandOutcome::Schema(Some(d))),
|
||||||
|
Command::ShowList { kind } => {
|
||||||
|
database.show_list(kind).await.map(CommandOutcome::ShowList)
|
||||||
|
}
|
||||||
Command::Insert {
|
Command::Insert {
|
||||||
table,
|
table,
|
||||||
columns,
|
columns,
|
||||||
|
|||||||
@@ -30,5 +30,6 @@ mod sql_drop_table;
|
|||||||
mod sql_insert;
|
mod sql_insert;
|
||||||
mod sql_select;
|
mod sql_select;
|
||||||
mod sql_update;
|
mod sql_update;
|
||||||
|
mod show_list;
|
||||||
mod undo_snapshots;
|
mod undo_snapshots;
|
||||||
mod walking_skeleton;
|
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",
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -233,6 +233,7 @@ fn command_kind_label(cmd: &rdbms_playground::dsl::Command) -> String {
|
|||||||
AddConstraint { .. } => "AddConstraint".into(),
|
AddConstraint { .. } => "AddConstraint".into(),
|
||||||
DropConstraint { .. } => "DropConstraint".into(),
|
DropConstraint { .. } => "DropConstraint".into(),
|
||||||
ShowTable { .. } => "ShowTable".into(),
|
ShowTable { .. } => "ShowTable".into(),
|
||||||
|
ShowList { kind } => format!("ShowList({kind:?})"),
|
||||||
Insert { .. } => "Insert".into(),
|
Insert { .. } => "Insert".into(),
|
||||||
Update { .. } => "Update".into(),
|
Update { .. } => "Update".into(),
|
||||||
Delete { .. } => "Delete".into(),
|
Delete { .. } => "Delete".into(),
|
||||||
|
|||||||
Reference in New Issue
Block a user