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
@@ -0,0 +1,172 @@
# ADR-0014: Data operations, value literals, and the auto-show pattern
## Status
Accepted
## Context
Schema operations (ADRs 0002, 0005, 0011, 0013) gave us tables,
columns, and relationships. Without INSERT / UPDATE / DELETE,
foreign-key behaviour is observable but not demonstrable — a
learner can't yet trigger a CASCADE or watch a constraint catch
a bad write. C5 closes that gap.
Several coupled questions:
- **Value literals.** How does the user write `'2025-01-15'` for
a date column versus `42` for an int column? What gets
validated where?
- **Safe defaults for destructive operations.** UPDATE and
DELETE without WHERE are classic foot-guns.
- **Auto-generation of `shortid`.** T2 commits to client-side
generation at insert time, which now becomes load-bearing.
- **FK error clarity.** SQLite reports `FOREIGN KEY constraint
failed` with no detail; pedagogically that's nearly useless.
- **Showing data back to the user.** Without a `SELECT`-like
surface, the user has no way to see what changed.
## Decision
### Grammar
```
insert into <Table> [(<col>, <col>...)] values (<val>, <val>...)
update <Table> set <col>=<val>[, <col>=<val>...] (where <col>=<val> | --all-rows)
delete from <Table> (where <col>=<val> | --all-rows)
show data <Table>
```
- **INSERT short form** (`insert into T values (...)`): values
apply to non-auto-generated columns in schema declaration
order. Serial columns are filled by SQLite; shortid columns
are auto-generated by the executor.
- **INSERT long form** (with explicit column list): user
controls exactly which columns receive values; auto-generated
columns the user didn't list are still auto-filled.
- **WHERE clause** is required for UPDATE and DELETE by default.
The `--all-rows` flag is the explicit opt-in to unfiltered
operations, following ADR-0009 (`--` reserved for opt-in
flags). Specifying both WHERE and `--all-rows` is a parse
error.
- **WHERE this iteration** is exactly `<col>=<val>`. Richer
WHERE expressions (AND/OR/comparison/LIKE) are deferred — they
are tracked as a future iteration and are intended as the
bridge from DSL into real SQL fluency.
- **`show data <Table>`** joins the V5 show-family (with
`show table <Name>`); auto-show after writes (below) means
most users won't need to call it explicitly.
### Value literals
The parser produces a small `Value` AST (`Number(String)`,
`Text(String)`, `Bool(bool)`, `Null`). Per-column-type validation
lives in the executor where the schema is known:
| User-facing type | Accepted literal |
|------------------|-------------------------------------------------------------|
| `text` | single-quoted string `'hello'` (`''` escapes a quote) |
| `int` | integer literal `42`, `-7` |
| `real` | numeric literal `3.14`, `-0.5` |
| `decimal` | numeric literal; stored as text to preserve precision |
| `bool` | `true` / `false` |
| `date` | quoted `'YYYY-MM-DD'` (validated) |
| `datetime` | quoted `'YYYY-MM-DDTHH:MM:SS[.fff][Z|±HH:MM]'` (validated) |
| `blob` | DSL literal not supported this iteration |
| `serial` | normally omitted (auto-fill); explicit integer accepted |
| `shortid` | normally omitted (auto-generated); explicit base58 1012 |
| `null` | keyword `null` |
Validation produces friendly errors that name the column and
expected shape — e.g. "column `Name` expects a quoted string for
`text`, got number".
### Auto-generation for `shortid`
When an INSERT (in either form) does not provide a value for a
`shortid` column, the executor calls the shortid generator and
fills in a 10-character base58 value (no `0`/`O`/`I`/`l`).
Explicit values are accepted but validated against the same
alphabet and length range (1012 chars). The `rand` crate is
the source of randomness.
### FK error enrichment
SQLite reports `FOREIGN KEY constraint failed` without naming
the offending constraint or value. The executor catches this
class of error and appends the table's outbound relationships
(via the metadata table from ADR-0013) to the message:
```
FOREIGN KEY constraint failed. Foreign keys on this table:
- Orders.CustId → Customers.id
Check that each referenced value exists in the parent table.
```
Identifying the *exact* offending row is left to the H1 friendly
error layer when that lands.
### Auto-show after writes
INSERT, UPDATE, and DELETE successfully completing fetch the
target table's full data and emit a `DslDataSucceeded` event
carrying both `rows_affected: Some(n)` and the data view. The
App renders both. Users see the result immediately without
needing a follow-up `show data` command.
`show data <Table>` follows the same path with
`rows_affected: None`.
The auto-show convention is reserved for DSL data ops. When
advanced-mode SQL lands (Q1), arbitrary `SELECT` statements may
opt out — large result sets shouldn't be implicitly inserted into
the session log.
### Tabular rendering
The data view is rendered as simple aligned-column text:
```
id | Name | Email
----+-----------+-----------------
1 | Alice | a@b.com
2 | Bob | (null)
```
Pretty box-drawing renderings (with truncation, scroll
indicators, wide-table handling) are deferred to V4. NULL cells
render as `(null)` to be explicitly visible; booleans render as
`true`/`false` despite their integer storage.
## Consequences
- C5 is satisfied: INSERT/UPDATE/DELETE operate end-to-end with
validation, auto-generation, FK enforcement, and visible
feedback.
- T2 is satisfied: shortid auto-generation runs on insert.
- V2 partial: a usable tabular data view exists, with the
pretty-rendering iteration still ahead.
- V5 partial: `show data` joins `show table` in the show family.
- H1 partial: FK-failure messages are enriched without
introducing the full friendly-error layer.
- The `--all-rows` opt-in convention is now established for
destructive-without-filter operations — future commands of the
same shape (`drop relationship --cascade`?) follow the
pattern.
- The runtime's `CommandOutcome` enum is extended with a `Data`
variant. New data-emitting commands plug in there without
reshaping the dispatch.
- Complex WHERE expressions (AND/OR/comparison/LIKE), bulk
INSERT, ORDER BY, LIMIT, JOIN, and SELECT in advanced mode are
explicitly out of scope for this iteration; richer DSL WHERE
is the bridge iteration toward Q1's full SQL handling.
## See also
- ADR-0005 (column types — value-literal mappings here mirror
the storage choices)
- ADR-0009 (DSL command syntax conventions — `--` flag rule)
- ADR-0011 (FK column type compatibility — used during the
validation that runs before writes)
- ADR-0013 (relationships and rebuild-table — the FK metadata
used by the error-enrichment path)
+1
View File
@@ -19,3 +19,4 @@ This directory contains the project's ADRs, recorded per
- [ADR-0011 — Foreign-key column type compatibility](0011-fk-column-type-compatibility.md)
- [ADR-0012 — Internal metadata for user-facing column types](0012-internal-metadata-for-user-facing-types.md)
- [ADR-0013 — Relationships, naming, and the rebuild-table strategy](0013-relationships-and-rebuild-table.md)
- [ADR-0014 — Data operations, value literals, and the auto-show pattern](0014-data-operations-and-value-model.md)
+34 -5
View File
@@ -132,7 +132,18 @@ against it.
- [ ] **C4** Convenience: `create m:n relationship from <T1> to
<T2>` produces an auto-named junction table the user can rename;
pulls primary keys and FK definitions automatically.
- [ ] **C5** Data operations: insert / update / delete via DSL.
- [x] **C5** Data operations: insert / update / delete via DSL.
*(ADR-0014. INSERT short and long forms, UPDATE/DELETE with
required WHERE plus `--all-rows` opt-in, `show data <T>`,
per-column-type value-literal validation, FK enforcement
with metadata-driven error enrichment, auto-show after
writes. Bulk insert, complex WHERE expressions, and SELECT
in advanced mode are explicitly tracked separately — see
C5a below.)*
- [~] **C5a** Complex WHERE expressions (AND/OR/comparison
operators/LIKE) for UPDATE/DELETE/show-data filtering. Tracks
the natural progression from DSL into real SQL fluency that
motivates the playground; design and ADR pending.
## SQL handling
@@ -170,10 +181,11 @@ against it.
`int`, `real`, `decimal`, `bool`, `date`, `datetime`, `blob`,
`serial`, `shortid`. *(Mapping to SQLite STRICT covered by
ADR-0005; FK target type rule by ADR-0011.)*
- [ ] **T2** `shortid` generation: base58, 1012 characters,
- [x] **T2** `shortid` generation: base58, 1012 characters,
omits ambiguous characters; generated client-side at insert.
*(Type exists; insert-time generation arrives with the data
insertion path.)*
*(Implemented per ADR-0014; auto-fills omitted shortid
columns and validates user-supplied values against the same
alphabet and length range.)*
- [ ] **T3** Compound primary keys handled end-to-end (DSL,
storage, display, FK reference).
*(Progress: DSL grammar (`with pk a:int,b:int`), storage, and
@@ -193,6 +205,10 @@ against it.
see V4 for the broader direction.)*
- [ ] **V2** SQL query results render as a dynamic table view in
the output pane, with multiple result tabs supported.
*(Progress: a basic aligned-column data view is rendered for
`show data` and after every write (ADR-0014). Pretty
box-drawing tables with truncation/scroll handling, plus
multi-tab support, remain in V4 territory.)*
- [~] **V3** Full ER-diagram export (whole-database graph, viewed
outside the TUI) — low priority; design and ADR pending.
- [~] **V4** Output panel as a *scrollable per-session log* with
@@ -210,7 +226,7 @@ against it.
styling, Markdown export, scroll indicator — remains pending.)*
- [ ] **V5** `show <kind> [<name>]` family of commands for
redisplaying schema info on demand. *(Progress: `show table
<name>` implemented and reuses the structure-render pipeline;
<name>` and `show data <Table>` implemented;
`show tables`, `show relationships`, etc. pending.)*
## Project lifecycle (per ADR-0004)
@@ -281,6 +297,19 @@ against it.
- [ ] **H1** Friendly error-rewriting layer translates SQLite
error messages into learner-friendly equivalents.
*(Progress: foreign-key constraint failures are enriched
with both inbound and outbound relationship listings (so
RESTRICT errors point at the children that still reference
this table); full SQL → English translation pending.)*
- [ ] **H1a** Strong syntax-help in parse errors. When the user
types something near-correct (e.g. `insert into T ('Oli')` —
forgotten `values`; or `update T set x=1` — missing WHERE),
the error should *name the missing keyword or clause* rather
than just point at the unexpected character. This is a
separate effort from H1 (which targets database errors); it
targets parser errors. Pending — multiple targeted fixes
shipping piecemeal so far (e.g. `values` becoming optional in
INSERT removes one such case).
- [ ] **H2** `hint` provides contextual help for the current
input or the most recent error.
- [ ] **H3** `help` provides general reference and per-command