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.
7.0 KiB
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 versus42for 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 failedwith 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-rowsflag is the explicit opt-in to unfiltered operations, following ADR-0009 (--reserved for opt-in flags). Specifying both WHERE and--all-rowsis 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 (withshow 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 |
blob |
DSL literal not supported this iteration |
serial |
normally omitted (auto-fill); explicit integer accepted |
shortid |
normally omitted (auto-generated); explicit base58 10–12 |
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 (10–12 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 datajoinsshow tablein the show family. - H1 partial: FK-failure messages are enriched without introducing the full friendly-error layer.
- The
--all-rowsopt-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
CommandOutcomeenum is extended with aDatavariant. 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)