feat: V5a show relationship/index <name> detail views

Fold the singular per-item forms into Command::ShowList { kind,
name: Option<String> } (name: Some = one item). Two grammar
branches reuse the relationship/index completion sources; worker
do_show_one renders a labelled detail block or a friendly
"No ... named X." line, reusing the V5 render path. Help +
parse-usage entries, two ADR-0042 near-miss rows, 5 integration
tests. Mark V5a [x] — V5's [<name>] clause now complete.
This commit is contained in:
claude@clouddev1
2026-06-07 14:04:00 +00:00
parent 757711f2bf
commit 1d898adf00
13 changed files with 272 additions and 32 deletions
+5 -2
View File
@@ -2890,6 +2890,8 @@ mod tests {
"show tables",
"show relationships",
"show indexes",
"show relationship",
"show index",
] {
app.update(key(KeyCode::Tab));
assert_eq!(app.input, expected);
@@ -2903,8 +2905,9 @@ mod tests {
fn shift_tab_cycles_backward_starting_from_last() {
let mut app = App::new();
type_str(&mut app, "show ");
// Backward starts from the last candidate (`indexes`).
for expected in ["show indexes", "show relationships", "show tables"] {
// Backward starts from the last candidate (`index`, the
// V5a singular form).
for expected in ["show index", "show relationship", "show indexes"] {
app.update(key(KeyCode::BackTab));
assert_eq!(app.input, expected);
}
+2
View File
@@ -1674,6 +1674,8 @@ mod tests {
"tables".to_string(),
"relationships".to_string(),
"indexes".to_string(),
"relationship".to_string(),
"index".to_string(),
],
);
}
+70 -3
View File
@@ -560,6 +560,9 @@ enum Request {
/// from the same helpers the items panel and describe view use.
ShowList {
kind: crate::dsl::command::ShowListKind,
/// `None` lists all items of the kind; `Some(name)` shows
/// one named relationship/index's detail (V5a).
name: Option<String>,
reply: oneshot::Sender<Result<Vec<String>, DbError>>,
},
DescribeTable {
@@ -1330,9 +1333,10 @@ impl Database {
pub async fn show_list(
&self,
kind: crate::dsl::command::ShowListKind,
name: Option<String>,
) -> Result<Vec<String>, DbError> {
let (reply, recv) = oneshot::channel();
self.send(Request::ShowList { kind, reply }).await?;
self.send(Request::ShowList { kind, name, reply }).await?;
recv.await.map_err(|_| DbError::WorkerGone)?
}
@@ -2264,8 +2268,8 @@ 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::ShowList { kind, name, reply } => {
let _ = reply.send(do_show_list(conn, kind, name.as_deref()));
}
Request::DescribeTable {
name,
@@ -5865,8 +5869,13 @@ fn do_list_tables(conn: &Connection) -> Result<Vec<String>, DbError> {
fn do_show_list(
conn: &Connection,
kind: crate::dsl::command::ShowListKind,
name: Option<&str>,
) -> Result<Vec<String>, DbError> {
use crate::dsl::command::ShowListKind;
// V5a: a named item shows one relationship/index's detail.
if let Some(name) = name {
return do_show_one(conn, kind, name);
}
let mut lines = Vec::new();
match kind {
ShowListKind::Tables => {
@@ -5929,6 +5938,64 @@ fn do_show_list(
Ok(lines)
}
/// Detail lines for one named relationship or index (V5a): a
/// labelled block, or a friendly "no such item" line. `Tables` is
/// never routed here (the table singular is `ShowTable`); the
/// defensive arm keeps the match total without a panic.
fn do_show_one(
conn: &Connection,
kind: crate::dsl::command::ShowListKind,
name: &str,
) -> Result<Vec<String>, DbError> {
use crate::dsl::command::ShowListKind;
let mut lines = Vec::new();
match kind {
ShowListKind::Relationships => match read_all_relationships(conn)?
.into_iter()
.find(|r| r.name == name)
{
None => lines.push(format!("No relationship named `{name}`.")),
Some(r) => {
lines.push(format!("Relationship `{}`:", r.name));
lines.push(format!(
" {}.{} → {}.{}",
r.parent_table, r.parent_column, r.child_table, r.child_column
));
lines.push(format!(" on delete {}", r.on_delete.keyword()));
lines.push(format!(" on update {}", r.on_update.keyword()));
}
},
ShowListKind::Indexes => {
// Find the user-created index by name across all tables.
let mut found = None;
for table in do_list_tables(conn)? {
if let Some(ix) = read_table_indexes(conn, &table)?
.into_iter()
.find(|ix| ix.name == name)
{
found = Some((table, ix));
break;
}
}
match found {
None => lines.push(format!("No index named `{name}`.")),
Some((table, ix)) => {
lines.push(format!("Index `{}` on {table}:", ix.name));
lines.push(format!(" columns: {}", ix.columns.join(", ")));
lines.push(format!(
" unique: {}",
if ix.unique { "yes" } else { "no" }
));
}
}
}
ShowListKind::Tables => {
lines.push(format!("No relationship or index named `{name}`."));
}
}
Ok(lines)
}
/// Internal full schema of a table, sufficient to regenerate
/// its `CREATE TABLE` statement during the rebuild dance.
#[derive(Debug, Clone)]
+19 -5
View File
@@ -332,12 +332,16 @@ 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.
/// Re-display a schema collection in the output (V5/V5a). With
/// `name: None`, the whole collection — every table,
/// relationship, or index (the list-all forms). With
/// `name: Some(_)`, one named item's detail — `show
/// relationship <name>` / `show index <name>` (V5a); the table
/// singular is the separate `ShowTable`, so a named `Tables`
/// never occurs. Read-only; pure display, no schema change.
ShowList {
kind: ShowListKind,
name: Option<String>,
},
/// Insert a single row. `columns` is `None` for the natural-
/// order short form (`insert into T values (...)`); the
@@ -903,7 +907,17 @@ impl Command {
Self::AddConstraint { .. } => "add constraint",
Self::DropConstraint { .. } => "drop constraint",
Self::ShowTable { .. } => "show table",
Self::ShowList { kind } => kind.command_name(),
// A named item reports the singular verb; the list-all
// forms report the plural (`kind.command_name()`).
Self::ShowList {
kind: ShowListKind::Relationships,
name: Some(_),
} => "show relationship",
Self::ShowList {
kind: ShowListKind::Indexes,
name: Some(_),
} => "show index",
Self::ShowList { kind, .. } => kind.command_name(),
Self::Insert { .. } => "insert into",
Self::Update { .. } => "update",
Self::Delete { .. } => "delete from",
+55
View File
@@ -107,12 +107,53 @@ 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"));
// `show relationship <name>` / `show index <name>` — singular
// per-item detail (V5a). The name slot reuses the existing
// completion sources (relationship / index names). Distinct
// keyword tokens from the plurals (`relationship` ≠
// `relationships`), so Choice ordering is irrelevant.
const SHOW_RELATIONSHIP_NAME: Node = Node::Ident {
source: IdentSource::Relationships,
role: "relationship_name",
validator: None,
highlight_override: None,
writes_table: false,
writes_column: false,
writes_user_listed_column: false,
writes_table_alias: false,
writes_cte_name: false,
writes_projection_alias: false,
};
const SHOW_RELATIONSHIP_NODES: &[Node] = &[
Node::Word(Word::keyword("relationship")),
SHOW_RELATIONSHIP_NAME,
];
const SHOW_RELATIONSHIP: Node = Node::Seq(SHOW_RELATIONSHIP_NODES);
const SHOW_INDEX_NAME: Node = Node::Ident {
source: IdentSource::Indexes,
role: "index_name",
validator: None,
highlight_override: None,
writes_table: false,
writes_column: false,
writes_user_listed_column: false,
writes_table_alias: false,
writes_cte_name: false,
writes_projection_alias: false,
};
const SHOW_INDEX_NODES: &[Node] =
&[Node::Word(Word::keyword("index")), SHOW_INDEX_NAME];
const SHOW_INDEX: Node = Node::Seq(SHOW_INDEX_NODES);
const SHOW_CHOICES: &[Node] = &[
SHOW_DATA,
SHOW_TABLE,
SHOW_TABLES,
SHOW_RELATIONSHIPS,
SHOW_INDEXES,
SHOW_RELATIONSHIP,
SHOW_INDEX,
];
const SHOW_SHAPE: Node = Node::Choice(SHOW_CHOICES);
@@ -576,12 +617,24 @@ fn build_show(path: &MatchedPath, _source: &str) -> Result<Command, ValidationEr
}),
Some("tables") => Ok(Command::ShowList {
kind: ShowListKind::Tables,
name: None,
}),
Some("relationships") => Ok(Command::ShowList {
kind: ShowListKind::Relationships,
name: None,
}),
Some("indexes") => Ok(Command::ShowList {
kind: ShowListKind::Indexes,
name: None,
}),
// V5a singular per-item detail — carry the named item.
Some("relationship") => Ok(Command::ShowList {
kind: ShowListKind::Relationships,
name: Some(require_ident(path, "relationship_name")?),
}),
Some("index") => Ok(Command::ShowList {
kind: ShowListKind::Indexes,
name: Some(require_ident(path, "index_name")?),
}),
_ => Err(ValidationError {
message_key: "parse.error_wrapper",
@@ -1395,6 +1448,8 @@ pub static SHOW: CommandNode = CommandNode {
"parse.usage.show_tables",
"parse.usage.show_relationships",
"parse.usage.show_indexes",
"parse.usage.show_relationship",
"parse.usage.show_index",
],};
pub static INSERT: CommandNode = CommandNode {
+2
View File
@@ -311,6 +311,8 @@ pub const KEYS_AND_PLACEHOLDERS: &[(&str, &[&str])] = &[
("parse.usage.show_tables", &[]),
("parse.usage.show_relationships", &[]),
("parse.usage.show_indexes", &[]),
("parse.usage.show_relationship", &[]),
("parse.usage.show_index", &[]),
("parse.usage.update", &[]),
("parse.usage.with", &[]),
("parse.expect.select_projection", &[]),
+4
View File
@@ -325,6 +325,8 @@ help:
show tables — list all tables
show relationships — list all relationships
show indexes — list all indexes
show relationship <name> — show one relationship's detail
show index <name> — show one index's detail
insert: |-
insert into <T> [(cols)] [values] (vals) — add a row
update: |-
@@ -565,6 +567,8 @@ parse:
show_tables: "show tables"
show_relationships: "show relationships"
show_indexes: "show indexes"
show_relationship: "show relationship <name>"
show_index: "show index <name>"
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)"
+2
View File
@@ -2061,6 +2061,8 @@ mod tests {
"tables".to_string(),
"relationships".to_string(),
"indexes".to_string(),
"relationship".to_string(),
"index".to_string(),
],
);
}
+4 -3
View File
@@ -2774,9 +2774,10 @@ 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::ShowList { kind, name } => database
.show_list(kind, name)
.await
.map(CommandOutcome::ShowList),
Command::Insert {
table,
columns,