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:
+64
-9
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user