INSERT/UPDATE/DELETE + value model + auto-show, with polish

DSL data operations (ADR-0014):
- insert into T [(cols)] values (vals); short form
  insert into T (vals) omits values keyword for friendlier
  syntax.
- update T set ... where col=val | --all-rows; delete from T
  where col=val | --all-rows; show data T.
- Value AST (Number/Text/Bool/Null) with per-column-type
  validation in the executor: int/real/decimal/bool/date/
  datetime/shortid each accept a documented literal shape
  and produce friendly format errors naming the column.
- INSERT short form fills non-auto-generated columns in
  schema order; auto-fills serial via SQLite and shortid
  via the new generator (T2).
- `add column [to table] T: c (type)` -- `to table` now
  optional.

Database:
- insert/update/delete via prepared statements with bound
  rusqlite::types::Value parameters.
- InsertResult/UpdateResult/DeleteResult: writes return
  rows_affected plus the affected row(s) only (not the whole
  table), so users see exactly what changed.
- INSERT shows the just-inserted row via last_insert_rowid.
- UPDATE captures matching rowids up-front and fetches them
  post-update -- works even if the UPDATE changed the WHERE
  column.
- DELETE reports per-relationship cascade effects by row-
  count diffing inbound child tables; UPDATE-side cascades
  are not yet detected (would need value diffing).
- query_data formats cells (booleans true/false, NULLs as
  None).

FK error enrichment:
- Now lists both outbound (INSERT/UPDATE relevance) and
  inbound (DELETE/UPDATE on parent relevance) FKs from the
  metadata, so RESTRICT errors point at the children
  blocking the delete.
- RelationshipSelector has a proper Display impl -- "no
  such relationship" reads cleanly.

Relationship display:
- target_table for AddRelationship/DropRelationship now
  returns the parent (1-side); structure rendering after
  add/drop shows that side's "Referenced by:" entry,
  matching the `from <Parent>` direction of the command.
- [ok] summary uses display_subject so relationship
  commands show both endpoints (`from P.col to C.col`)
  rather than a single misleading table name.
- Auto-name format `<Parent>_<pcol>_to_<Child>_<ccol>`
  (matches the from..to direction).

Output rendering and scrolling:
- Wrap-aware scroll: renderer reports both visible-row
  count and total wrapped-row count to App; scroll math
  caps against actual displayable rows. Long lines wrap;
  the bottom line is always reachable; PageUp/PageDown work
  correctly even after paging past the buffer top.
- Multi-line messages (FK error enrichment, cascade summary)
  split into single-line OutputLines at creation time so
  wrap/scroll math agree.

Runtime / events:
- New AppEvent variants for Insert/Update/Delete success
  carrying typed result structs; DslDataSucceeded reserved
  for show-data queries.

Docs:
- ADR-0014 covers data-op grammar, value model, --all-rows
  safety, auto-show.
- requirements.md: C5 done, T2 done, V2 partial (basic data
  view), V5 partial (show data added). New entries: C5a
  complex WHERE expressions; H1 progress note for FK
  enrichment; H1a (strong syntax-help in parse errors).

Tests: 200 passing (183 lib + 17 integration), 0 skipped.
Includes parser, type-validation, DB write/read, FK-failure
enrichment, cascade-delete propagation, focused-auto-show
behaviour, scroll-cap invariants. Clippy clean with nursery
enabled.
This commit is contained in:
claude@clouddev1
2026-05-07 16:33:25 +00:00
parent 165068269b
commit 305e5083d5
16 changed files with 2638 additions and 109 deletions
+64 -9
View File
@@ -26,7 +26,9 @@ use tracing::{debug, error, info, warn};
use crate::action::Action;
use crate::app::App;
use crate::db::{Database, DbError, TableDescription};
use crate::db::{
DataResult, Database, DbError, DeleteResult, InsertResult, TableDescription, UpdateResult,
};
use crate::dsl::Command;
use crate::event::AppEvent;
use crate::theme::Theme;
@@ -121,10 +123,26 @@ fn spawn_dsl_dispatch(
tokio::spawn(async move {
let outcome = execute_command(&database, command.clone()).await;
let event = match outcome {
Ok(description) => AppEvent::DslSucceeded {
Ok(CommandOutcome::Schema(description)) => AppEvent::DslSucceeded {
command: command.clone(),
description,
},
Ok(CommandOutcome::Query(data)) => AppEvent::DslDataSucceeded {
command: command.clone(),
data,
},
Ok(CommandOutcome::Insert(result)) => AppEvent::DslInsertSucceeded {
command: command.clone(),
result,
},
Ok(CommandOutcome::Update(result)) => AppEvent::DslUpdateSucceeded {
command: command.clone(),
result,
},
Ok(CommandOutcome::Delete(result)) => AppEvent::DslDeleteSucceeded {
command: command.clone(),
result,
},
Err(error) => AppEvent::DslFailed {
command: command.clone(),
error,
@@ -141,15 +159,23 @@ fn spawn_dsl_dispatch(
Ok(tables) => {
let _ = event_tx.send(AppEvent::TablesRefreshed(tables)).await;
}
Err(e) => warn!(error = %e, "post-DDL list_tables failed"),
Err(e) => warn!(error = %e, "post-list_tables failed"),
}
});
}
enum CommandOutcome {
Schema(Option<TableDescription>),
Query(DataResult),
Insert(InsertResult),
Update(UpdateResult),
Delete(DeleteResult),
}
async fn execute_command(
database: &Database,
command: Command,
) -> Result<Option<TableDescription>, String> {
) -> Result<CommandOutcome, String> {
match command {
Command::CreateTable {
name,
@@ -158,17 +184,17 @@ async fn execute_command(
} => database
.create_table(name, columns, primary_key)
.await
.map(Some)
.map(|d| CommandOutcome::Schema(Some(d)))
.map_err(friendly),
Command::DropTable { name } => database
.drop_table(name)
.await
.map(|()| None)
.map(|()| CommandOutcome::Schema(None))
.map_err(friendly),
Command::AddColumn { table, column, ty } => database
.add_column(table, column, ty)
.await
.map(Some)
.map(|d| CommandOutcome::Schema(Some(d)))
.map_err(friendly),
Command::AddRelationship {
name,
@@ -191,16 +217,45 @@ async fn execute_command(
create_fk,
)
.await
.map(Some)
.map(|d| CommandOutcome::Schema(Some(d)))
.map_err(friendly),
Command::DropRelationship { selector } => database
.drop_relationship(selector)
.await
.map(CommandOutcome::Schema)
.map_err(friendly),
Command::ShowTable { name } => database
.describe_table(name)
.await
.map(Some)
.map(|d| CommandOutcome::Schema(Some(d)))
.map_err(friendly),
Command::Insert {
table,
columns,
values,
} => database
.insert(table, columns, values)
.await
.map(CommandOutcome::Insert)
.map_err(friendly),
Command::Update {
table,
assignments,
filter,
} => database
.update(table, assignments, filter)
.await
.map(CommandOutcome::Update)
.map_err(friendly),
Command::Delete { table, filter } => database
.delete(table, filter)
.await
.map(CommandOutcome::Delete)
.map_err(friendly),
Command::ShowData { name } => database
.query_data(name)
.await
.map(CommandOutcome::Query)
.map_err(friendly),
}
}