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
-11
@@ -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
@@ -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]
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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", &[]),
|
||||
|
||||
@@ -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
@@ -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]
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user