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:
claude@clouddev1
2026-06-07 13:20:52 +00:00
parent 28e75961aa
commit 8dec784080
15 changed files with 555 additions and 27 deletions
+30 -11
View File
@@ -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 <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 —
// 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]
+14 -2
View File
@@ -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]
+95
View File
@@ -554,6 +554,14 @@ enum Request {
ListTables {
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 {
name: String,
source: Option<String>,
@@ -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<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(
&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<Vec<String>, DbError> {
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
/// its `CREATE TABLE` statement during the rebuild dance.
#[derive(Debug, Clone)]
+42
View File
@@ -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<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).
///
/// 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 <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
// CSV re-persistence and ok-summary subject).
Self::SqlInsert { target_table, .. }
+38 -5
View File
@@ -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<Command, ValidationEr
_ => 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"),
+1 -1
View File
@@ -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;
+3
View File
@@ -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 <kind>` list command (V5) — carries pre-formatted
/// display lines (tables / relationships / indexes).
DslShowListSucceeded { command: Command, lines: Vec<String> },
DslInsertSucceeded {
command: Command,
result: InsertResult,
+3
View File
@@ -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", &[]),
+6
View File
@@ -317,6 +317,9 @@ help:
show: |-
show table <T> — show a table's structure
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 into <T> [(cols)] [values] (vals) — add a row
update: |-
@@ -554,6 +557,9 @@ parse:
[--force-conversion | --dont-convert]
show_data: "show data <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>[, ...])"
update: "update <Table> set <col>=<value>[, ...] (where <col>=<value> | --all-rows)"
delete: "delete from <Table> (where <col>=<value> | --all-rows)"
+12 -2
View File
@@ -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]
+11
View File
@@ -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 <kind>` list (V5) — pre-formatted display lines from
/// the worker (table / relationship / index names). Pure
/// display, no schema change.
ShowList(Vec<String>),
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,