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
+59 -17
View File
@@ -129,31 +129,73 @@ fn render_output_panel(app: &mut App, theme: &Theme, frame: &mut Frame<'_>, area
vertical: 1,
});
// Show a window of the buffer ending `output_scroll` lines
// above the most recent entry. With scroll == 0 the last
// line shown is the most recent; PageUp increases the
// scroll, revealing older lines. We report the visible row
// count back to App so input handling can cap scroll
// correctly between renders (otherwise scroll could drift
// past the top and slide the window off).
// Render every output line into a wrapped Paragraph and let
// ratatui handle the wrapping; we then use the wrapped row
// count to cap scroll correctly. Bottom-anchoring (most
// recent visible by default) is achieved by computing the
// scroll offset relative to the bottom of the wrapped view.
let visible = inner.height as usize;
app.note_output_viewport(visible);
let total = app.output.len();
let max_scroll = total.saturating_sub(visible);
let effective_scroll = app.output_scroll.min(max_scroll);
let end = total - effective_scroll;
let start = end.saturating_sub(visible);
// Compute the total wrapped row count first, working from
// OutputLines directly (so the borrow ends before the
// mutable `note_output_viewport` call below).
let total_wrapped = approximate_wrapped_rows_from_output(&app.output, inner.width);
app.note_output_viewport(visible, total_wrapped);
let lines: Vec<Line<'_>> = app
.output
.iter()
.skip(start)
.take(end - start)
.map(|line| render_output_line(line, theme))
.collect();
let paragraph = Paragraph::new(lines).wrap(Wrap { trim: false });
let max_scroll = total_wrapped.saturating_sub(visible);
let effective_scroll = app.output_scroll.min(max_scroll);
// Paragraph::scroll((y, _)) sets the topmost visible row.
// We want bottom-anchored: y = max_scroll - effective_scroll
// so scroll==0 shows the bottom and PageUp moves y down to
// reveal older content.
let scroll_y = max_scroll.saturating_sub(effective_scroll);
let scroll_y_u16 = u16::try_from(scroll_y).unwrap_or(u16::MAX);
frame.render_widget(block, area);
let paragraph = Paragraph::new(lines).wrap(Wrap { trim: false });
frame.render_widget(paragraph, inner);
frame.render_widget(paragraph.scroll((scroll_y_u16, 0)), inner);
}
/// Approximate the number of display rows the output buffer
/// will occupy after width-wrapping. Computed directly from
/// `OutputLine`s (rather than the rendered `Line` objects) so
/// the calculation can run before we hand `&mut App` to the
/// scroll-cap update. Ratatui's exact `line_count` is gated
/// behind an unstable feature; this character-based
/// approximation is close enough for scroll capping — off-by-one
/// at boundaries is acceptable.
fn approximate_wrapped_rows_from_output(
output: &std::collections::VecDeque<OutputLine>,
width: u16,
) -> usize {
if width == 0 {
return output.len();
}
let w = usize::from(width);
output
.iter()
.map(|line| {
// Tag width matches `render_output_line` exactly so
// the row count is right when the panel is too narrow
// for the natural line.
let tag_len = match line.kind {
OutputKind::Echo => match line.mode_at_submission {
Mode::Simple => "[simple] ".len(),
Mode::Advanced => "[advanced] ".len(),
},
OutputKind::System => "[system] ".len(),
OutputKind::Error => "[error] ".len(),
};
let total = tag_len + line.text.chars().count();
if total == 0 { 1 } else { total.div_ceil(w) }
})
.sum()
}
fn render_output_line<'a>(line: &'a OutputLine, theme: &Theme) -> Line<'a> {