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:
@@ -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 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 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)
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user