Commit Graph

380 Commits

Author SHA1 Message Date
claude@clouddev1 0d7a7bcd49 db: end-to-end tests for change_column int -> bool (B2)
The (Int, Bool) entry of the ADR-0017 §3 matrix was already
covered at the per-cell unit-test level in `type_change.rs`,
but the end-to-end change_column path through `db.rs` had no
test exercising it. This closes that gap with the two cases
called out in the handoff:

- `change_column_type_int_to_bool_with_zero_one_succeeds`:
  Rows 0/1/0 succeed, no [client-side] note. The matrix
  returns the same Value::Integer for 0 and 1, so
  is_non_identity reports false for every cell and
  ClientSideNote.transformed stays at 0 — the
  `transformed > 0 || auto_filled > 0` filter therefore
  drops the note.
- `change_column_type_int_to_bool_refuses_other_values`:
  Row with 2 → Incompatible. Verified under both Default
  and ForceConversion modes (per ADR-0017 §5: incompatible
  is not lossy, --force-conversion must not advertise).

No production code change; tests only. 534 -> 536 passing,
clippy clean with nursery lints enabled.
2026-05-08 14:49:34 +00:00
claude@clouddev1 dcfeef5d3c Handoff doc for end of 2026-05-08 (#5) 2026-05-08 14:39:54 +00:00
claude@clouddev1 5bb0a147f0 ADR-0018 implementation: auto-fill contracts for serial and shortid
Generalises serial and shortid beyond their previous restricted
forms:

- `serial` is no longer restricted to single-column PK. Non-PK
  serial columns get an emitted UNIQUE constraint and use
  application-side MAX(col)+1 at INSERT time (rowid alias still
  drives the PK case for free; per ADR-0010 worker-thread
  serialisation, the read-then-insert sequence is safe).
- `shortid` columns auto-fill existing null cells when the
  column is materialised — `add column T: x (shortid)` on a
  non-empty table no longer leaves rows in a not-really-valid
  NULL state.
- `int -> serial` joins the type-change matrix as always-clean
  identity (closes the asymmetry vs `text -> shortid`); other
  sources are refused with a route-via-int hint.
- `change column T: x (serial|shortid)` fills null source
  cells with sequence / generated values in the same rebuild
  transaction.

Internal infrastructure:

- ReadColumn gains `unique: bool`; read_schema detects single-
  column UNIQUE indexes via pragma_index_list /
  pragma_index_info; schema_to_ddl emits inline UNIQUE for
  non-PK columns.
- ColumnSchema (persistence) gains `unique: bool` so the flag
  survives YAML round-trip and rebuild-from-text reconstructs
  it faithfully — preserves the "serial -> int leaves UNIQUE
  in place" promise across save/load cycles.
- ChangeColumnTypeResult.client_side now carries `auto_filled`
  + `auto_fill_kind` alongside `transformed` + `lossy`; the
  app handler renders separate note lines when both apply.
- AddColumnResult is a new return type carrying pre-rendered
  [client-side] note lines for the auto-fill paths.

Tests: 519 -> 534 (+15). Clippy clean.
2026-05-08 14:32:19 +00:00
claude@clouddev1 7dfa718c6e parser: structural error rendering, source echo, and caret pointer
The old humanise() fell back to chumsky's terse Display for non-
custom errors and appended "(near `X`)", which on top of an
already-cryptic "found 'i' expected ':'" turned the message into
a puzzle. Now humanise() reads the structured RichReason, lists
expected RichPatterns in plain prose, and prefixes the message
with the consumed context.

Before:  parse error: found 'i' expected ':' (near `i`)
After:   parse error: after `change column Rich`, expected `:`,
         found `i`

dispatch_dsl additionally echoes the source line on parse failure
(matching the success path's "running: ...") and prints a `^` caret
under the failure position, so the user can see what got submitted
and where the parser broke without re-reading from scratch.

Known limit: keyword_ci's custom-error mismatches don't aggregate
across choice alternatives, so messages like "expected DATA or
TABLE" (bison-equivalent) aren't yet possible. That's a structural
fix to the keyword matcher, deferred to a future parser-affordances
ADR.

Tests: +2 structural-error regression tests.
2026-05-08 13:21:39 +00:00
claude@clouddev1 00947b928c ADR-0017 implementation: per-cell type-change with override flags
Replaces the placeholder "trust STRICT" body of do_change_column_type
with the per-cell transformer matrix from ADR-0017. Adds:

- src/type_change.rs: CellOutcome { Clean / Lossy / Incompatible }
  + transform_cell + static_refusal covering every matrix pair
  from §3 (54 unit tests).
- --force-conversion and --dont-convert flags on `change column`
  (mutually exclusive at parse time per §5).
- Refined PK rule (§4.1): refused only when the column has an
  inbound FK and fk_target_type would change. Outbound-FK columns
  still refused outright (§4.2). PK / shortid uniqueness checked
  post-transformation (§4.3).
- Bordered diagnostic tables (lossy / incompatible / collision)
  via the pretty-table renderer (§7) — uses ADR-0016's primitives.
- [client-side] success note (§6) when any cell was rewritten.
- Friendly wrapper for engine-level errors under --dont-convert
  so no engine vocabulary leaks (ADR-0002 user-facing posture).

ADR-0017 §3 + §7 amended in place (with user sign-off): serial->int
added explicitly to the always-clean matrix, and diagnostic rows
identify themselves by PK value(s) rather than positional indices
(SQLite returns rows unordered without ORDER BY, so positional
"row 5" is unaddressable).

Tests: 449 -> 517 (+68). Clippy clean with nursery lints.
2026-05-08 13:21:07 +00:00
claude@clouddev1 545cbf5c0e Handoff doc for end of 2026-05-08 (#4)
Records the session's commits (Iter 5/6 follow-ups, pretty
rendering, B2/C2 column ops, ADR-0016, ADR-0017, ADR-0002
amendment, CLAUDE.md no-engine-name rule), the 449-test
baseline, and the next session's priority: implement
ADR-0017 (per-cell classification, FK-target-type-aware PK
precondition, uniqueness checks for PK + shortid,
--force-conversion / --dont-convert flags, pretty-table
rendered diagnostics).
2026-05-08 10:57:11 +00:00
claude@clouddev1 c3e5f9014f ADR-0017 + ADR-0002 amendment: type-change compatibility + engine-agnostic posture
Specifies the curated per-cell classification (clean /
lossy / incompatible) for column type changes, the static
transformer matrix (numeric chains, text↔structured types,
always-clean stringifications), and the PK / shortid /
uniqueness-bearing handling. Replaces the B2/C2
placeholder of "rely on engine STRICT and surface its
errors" with a learner-friendly model that:

* refuses incompatibles up-front,
* refuses lossy conversions by default with a re-run-with-
  --force-conversion hint,
* refines the PK refusal: an inbound-FK PK is only refused
  when the new type would change the FK target type
  (so `serial → int` and `shortid → text` on FK-referenced
  PKs are allowed; `int → text` etc. still refuse),
* adds a post-transformation uniqueness check for PK and
  shortid columns,
* uses the pretty-table renderer (ADR-0016) for all
  diagnostic row lists,
* emits a `[client-side] …` note in the success summary
  whenever the transformer rewrote any cell.

`--force-conversion` accepts loss; `--dont-convert` skips
the client-side layer entirely; mutually exclusive.

Forward-look: a future iteration may add resolution flags
(`--default 0`, `--on-incompatible '<value>'`).

Also amends ADR-0002 with a new "User-facing posture"
section cementing that the database engine choice is an
implementation detail and is never named in user-visible
strings. Adds a corresponding bullet to CLAUDE.md's
working-style rules so every session picks it up.

Implementation lands as a follow-up.
2026-05-08 10:53:20 +00:00
claude@clouddev1 7b97786ab7 B2/C2: column drop / rename / change-type DSL commands
Closes B2 (rebuild-table reused outside relationships) and
C2 (full add/drop/rename/change-type column operations).

* drop column [from] [table] <T>: <col>
  - ALTER TABLE DROP COLUMN (SQLite 3.35+) + metadata
    cleanup in __rdbms_playground_columns.
  - Refuses PK columns and columns involved in a declared
    relationship (drop the relationship first).

* rename column [in] [table] <T>: <old> to <new>
  - ALTER TABLE RENAME COLUMN (SQLite 3.25+); SQLite
    cascades the rename through FK declarations on other
    tables.
  - Mirrors the new name into both metadata tables
    (__rdbms_playground_columns, __rdbms_playground_relationships)
    so describes stay accurate after a rename.
  - Refuses identity rename and name collisions.

* change column [in] [table] <T>: <col> (<newtype>)
  - Routes through the rebuild_table primitive (ADR-0013)
    since SQLite ALTER doesn't support type changes.
    INSERT INTO new SELECT FROM old; STRICT typing enforces
    cell compatibility, transaction rolls back on mismatch.
  - Refuses PK columns, relationship-involved columns,
    `serial` target, and no-op same-type changes.

Adds 20 tests (parser + db layer); updates the in-app help
listing. Both prepositions independently optional in each
new command, matching `add column`'s grammar shape.

Total: 449 passing, 0 failing, 0 skipped (up from 429).
Clippy clean.

Known spec gap: column-type-change conversion compatibility
is not yet documented (currently relies on SQLite STRICT
errors); follow-up will close this.
2026-05-08 10:09:24 +00:00
claude@clouddev1 41cef5399b parser: make to and table independently optional in add column
Previously the grammar accepted only `to table` together or
neither. The user-stated convention is that bare table
identifiers are accepted in unambiguous positions (matching
how `add 1:n relationship from <T>.<col> to <T>.<col>` takes
bare table names). Both `to` and `table` are now or_not'd
independently, so all four combinations parse identically.

Updates the in-app `help` listing to advertise the new
shape: `add column [to] [table] <T>: <col> (<type>)`.

3 new parser tests cover the variants.
2026-05-08 09:16:50 +00:00
claude@clouddev1 1b27a0c9b1 runtime: suppress silent-rebuild banner for empty projects
A fresh-launch temp project enters run() with no .db, calls
rebuild_from_text on an empty schema, and used to surface a
"[ok] rebuild — 0 tables and 0 rows will be reconstructed"
note that conveyed no information. Now we only emit the note
when the project actually has tables. The explicit `rebuild`
command still always reports its summary — the user asked.
2026-05-08 09:15:19 +00:00
claude@clouddev1 5b5e08d852 ADR-0016 + Iter 5/6 follow-up: pretty table rendering
Replaces the placeholder pipe-and-dash output with Unicode
box-drawing tables for both data results and table-structure
listings, per ADR-0016.

* New `src/output_render.rs` module with `render_data_table`
  and `render_structure`. Hand-rolled to match the project's
  existing CSV/YAML pattern; ~300 lines.
* Header-only outer-frame border style: outer ┌─┐│└─┘ box +
  ├─┤ header underline, no per-row separators. NULL renders
  as `(null)`; cell newlines/tabs/control chars become
  `↵`/`→`/`·` as display-only substitutions.
* Type-aware column alignment: numeric types right-aligned,
  everything else left. `DataResult` gains a `column_types:
  Vec<Option<Type>>` field, populated from the existing
  metadata lookup at the two query sites in db.rs (no new
  query paths).
* Structure view shows Name | Type | Constraints columns;
  References / Referenced-by sections retain plain-text
  format, leaving room for the future relationship-rendering
  ADR.
* 18 new unit tests in output_render.rs (plus 4 insta
  snapshots for the canonical layouts). Existing assertions
  in app.rs and walking_skeleton.rs updated to match the new
  format.

Total: 426 passing, 0 failing, 0 skipped (up from 408).
Clippy clean.
2026-05-08 09:06:02 +00:00
claude@clouddev1 67d68db5f8 Iteration 6: --resume + persistent input history + migration scaffold
Closes out track 2's ADR-0015 backlog.

* `--resume` CLI flag (L1a, ADR-0015 §7) opens the most-
  recently-used project, tracked in <data-root>/last_project.
  Mutually exclusive with a positional <project-path>; errors
  cleanly to stderr (above the shell prompt) on missing file
  or stale recorded path. last_project is rewritten on every
  successful project open (startup, load, new, save as,
  import).
* Persistent input history (I2-persist, ADR-0015 §12). On
  project open, the in-memory navigable history is hydrated
  from the tail of history.log (capped at the in-memory cap).
  ProjectSwitched gains a `history_entries` payload field;
  App::seed_history is the entry point. Pipes inside source
  text round-trip via splitn(3); unknown escape sequences are
  passed through literally.
* Migration framework scaffold (F3, ADR-0015 §9). New
  persistence::migrations module with MigratorRegistry +
  migrate_to_latest + ensure_project_yaml_migrated. Empty
  in v1 (production registry has no migrators); the loader
  runs through it on every project open and is exercised by
  tests with a fake v1→v2 migrator. Writes
  project.yaml.v<N>.bak before any migrator runs; verifies
  each step bumps the version field.

Refreshes docs/requirements.md (A1 / I2 / F3 / E1 / L1a /
test baseline) and adds docs/handoff/20260508-handoff-3.md
covering both Iter 5 and Iter 6.

Total tests: 408 passing, 0 failing, 0 skipped (up from 345
at handoff-2). Clippy clean.
2026-05-08 08:27:50 +00:00
claude@clouddev1 c6cf3df6dc Iteration 5: export / import commands
Implements the `export` and `import` app-level commands per
ADR-0015 §11 + ADR-0007 amendment 1.

- `export [<path>]` writes a zip of project.yaml + data/ to
  <data-root>/YYYYMMDD-<projectname>-export-NN.zip by default,
  preserving the project's directory name as the single
  top-level folder inside the archive.
- `import <zip> [as <target>]` extracts an exported zip into
  a new named project and switches to it. Target name is
  derived from the zip's top-level folder by default; on
  collision the destination auto-suffixes -02, -03, ... up
  to -99 instead of refusing (deviates from §2's refuse-on-
  collision rule for save/save as; recorded as an amendment
  to ADR-0015 §11).
- Excludes playground.db and history.log from the zip.
- Path-traversal protection via zip::enclosed_name + post-
  resolution check that the extraction path stays inside
  the target directory.

Adds the zip = "5" dep with default-features = false +
features = ["deflate"] to keep the binary-size cost modest.

Test baseline: 370 passing, 0 failing, 0 skipped.
2026-05-08 08:24:45 +00:00
claude@clouddev1 ca71184678 Handoff doc + CLAUDE.md and requirements.md refresh
Adds docs/handoff/20260508-handoff-2.md describing the state at
the end of this session: ADR-0015 designed, Iterations 1-4 of
track 2 shipped (file-backed projects with auto-named [temp]
dirs, per-command write-through, rebuild from text on missing
.db, save/save as/new/load/rebuild commands with modal dialogs
and project switching), plus the cleanup pass (--help, in-app
help, post-rebuild message, unmodified-temp cleanup) and the
safety hardening of safely_delete_temp_project. Lists the
next-up moves (Iteration 5: export/import, Iteration 6:
--resume + persistent input history + migration scaffold) and
an end-to-end smoke test.

requirements.md: marks P1-P5, P-NAME-1/2/3, F1, F2, U3, L1 as
[x] with iteration references; adds P-CLEAN-1 for the safe
cleanup; updates A1, I2, H3, L1a progress notes.

CLAUDE.md: updates the project-storage decisions and
deferred-items entry to reflect what's now live vs. still
pending.
2026-05-08 07:07:38 +00:00
claude@clouddev1 58a964da8c Harden temp-project cleanup with stacked safety guards
The previous remove_dir_all on a path returned by Project::path()
was too trusting: an unusual CLI argument or a hand-edited
project.yaml could in principle have steered cleanup into
deleting the wrong directory. Replace it with
safely_delete_temp_project, which refuses unless every one of
the following passes:

1. Path is not a symlink (checked before canonicalize so a
   symlink can't smuggle a different target through).
2. Path is a directory.
3. Canonical path is under <active-data-root>/projects/
   (canonical-prefix containment).
4. Directory basename contains the literal `[temp]` marker.
5. Direct children are exclusively well-known project
   artefacts (project.yaml, data/, history.log,
   playground.db, .gitignore, lock file) plus migration .bak
   files and atomic-write .tmp files. Any stranger file
   (notes.md, .git/, screenshots, etc.) makes the helper
   refuse.

is_unmodified_temp now also requires data/ to be empty, in
addition to project.yaml's tables and relationships being
empty. A hand-edited yaml that drops the schema list but
leaves CSVs in data/ no longer passes.

Failure to delete is non-fatal -- the helper returns
SafeDeleteError, the runtime logs a tracing::warn!, and the
project stays on disk. Leaving an unexpected directory alone
is always preferable to a wrong delete.

Tests: 345 passing (272 lib + 9 + 5 + 6 + 27 + 9 + 17),
0 failing, 0 skipped. 7 new tests covering each guard,
including a unix-only symlink-rejection test.
2026-05-08 06:59:26 +00:00
claude@clouddev1 b7addd6161 Cleanup pass: --help, in-app help, post-rebuild message, unmodified-temp cleanup
Four post-Iteration-4 polish items surfaced by manual testing.

1. `--help` / `-h` CLI flag prints a usage banner (options +
   app-level commands + DSL grammar reference) and exits. Parse
   errors also print the banner to stderr.

2. `help` app-level command notes the same list of supported
   commands to the output panel -- a simple stand-in for the
   richer H3 help system, kept in sync with what's actually
   wired up.

3. The silent rebuild that runs when playground.db is missing
   now surfaces a system message in the output panel ("[ok]
   rebuild -- N tables, M rows reconstructed; ...") via a new
   initial_events plumbing. The user no longer wonders whether
   the .db was magically restored or whether anything happened
   on launch.

4. Unmodified empty temp projects (kind=Temp, project.yaml has
   tables: [] and relationships: []) are now auto-deleted when
   the user switches away (load / new / save as) or quits. This
   addresses the "launch app, load existing project, quit"
   pattern that was leaving an empty temp directory behind
   every time. Modified temps (with any user-created tables or
   relationships) are never auto-deleted; corrupted projects
   are also never auto-deleted (defensive default-to-false on
   yaml read/parse errors).

Tests: 338 passing (272 lib + 9 + 5 + 6 + 20 + 9 + 17),
0 failing, 0 skipped. Clippy clean.
2026-05-08 06:43:49 +00:00
claude@clouddev1 f2198275f0 Iteration 4b: save / save as / new / load with project switching
Adds the rest of the track-2 lifecycle commands (ADR-0015 §11)
and the project-switching machinery they need at runtime.

Temp vs named distinction: replaced the fragile naming heuristic
with an explicit `[temp]` marker in the directory pattern
(`<YYYYMMDD>-[temp]-<word>-<word>-<word>`). validate_user_name
already rejects brackets, so user-typed names can never collide
with a temp marker. The status bar shows `[TEMP] <Display Name>`
for temp projects; the prettifier strips both the date and the
marker so display names are clean.

save / save as: temp project's `save` opens a path-entry modal
(acts as save as); named project's `save` reports "already
auto-saved; use `save as`". `save as` always prompts. Relative
names resolve under <data-root>/projects/; absolute paths used
as-is. Copy excludes the per-process lock file; everything else
(.db, yaml, csvs, history.log) is copied.

new: closes current project, creates a fresh auto-named temp,
switches.

load: opens a picker. List sub-mode shows projects in the active
data root, sorted newest-first by project.yaml mtime; arrow keys
navigate, Enter loads, `b` switches to a path-entry sub-mode for
projects elsewhere, Esc cancels. Empty data root jumps straight
to path entry.

Runtime: `Session` holds Option<Project> + Option<Database> so
project switches can drop old (releasing lock + stopping worker)
before opening new -- required for the "load my own current
project" case. `perform_switch` handles Load / SaveAs / NewTemp
uniformly.

Tests: 332 passing (270 lib + 9 + 5 + 6 + 16 new + 9 + 17),
0 failing, 0 skipped. Clippy clean.
2026-05-08 06:23:46 +00:00
claude@clouddev1 ba93d3c7d8 Iteration 4a: rebuild command with confirmation modal
Adds the explicit `rebuild` app-level command (ADR-0015 §7, §11)
and a modal UI infrastructure to host its confirmation dialog.
Typing `rebuild` emits Action::PrepareRebuild; the runtime reads
project.yaml + data/ to compute a summary ("3 tables and 47 rows
will be reconstructed; the existing playground.db will be
replaced") and posts AppEvent::RebuildPrepared, which opens the
modal. Y confirms, N/Esc cancels. While the modal is open,
normal input is gated.

The worker's do_rebuild_from_text now wipes existing user tables
and metadata before reloading from text, so it works on both
fresh and populated databases. Source text is plumbed through
rebuild_from_text so the explicit rebuild logs to history.log
while the silent on-load rebuild from Iteration 3 stays silent.

Modal infrastructure (App.modal field + key routing + centered
overlay rendering + word-wrap) is reused by Iteration 4b's save
/ save as / load / new flows.

Tests: 314 passing (268 lib + 9 + 5 + 6 new + 9 + 17),
0 failing, 0 skipped. Clippy clean.
2026-05-07 22:27:37 +00:00
claude@clouddev1 f0fc063756 Iteration 3: existence-only load + rebuild from text on missing .db
When the runtime opens a project whose playground.db is missing,
it now rebuilds the database from project.yaml + data/<table>.csv
per ADR-0015 §7. The rebuild path:

1. Parses project.yaml (serde_yml). Unknown versions / types /
   actions surface as PersistenceFatal.
2. Recreates each user table with FK constraints inline
   (PRAGMA foreign_keys=OFF), then populates the column-type,
   relationship, and project metadata tables.
3. Loads each table's CSV via a hand-rolled reader that
   preserves the NULL-vs-empty distinction (the csv crate
   doesn't expose whether a field was quoted; ours does).
4. Runs PRAGMA foreign_key_check before commit; any violation
   aborts.
5. Restores foreign_keys=ON regardless of success.

Row-level failures get DbError::RebuildRowFailed with row
number, file, table, and a friendly per-type detail. They land
in the runtime as a fatal stderr message ("unable to load row N
from `data/T.csv` into table `T`: ...") before the alternate
screen is entered.

created_at from project.yaml overwrites the configure-time
placeholder so timestamps round-trip stably.

Tests: 307 passing (267 lib + 9 + 5 new + 9 + 17), 0 failing,
0 skipped. Clippy clean with nursery lints.
2026-05-07 22:11:45 +00:00
claude@clouddev1 5410075398 Persistence: empty table -> no CSV (per Iteration 2 follow-up)
The Iteration-2 rule wrote a header-only CSV for every existing
table, which surprised users who created a table and saw a file
appear before any data went in. Tighten the rule: a CSV exists
iff the table has rows. Persistence::write_table_data now
delegates to delete_table_data when the snapshot is empty,
removing any prior CSV. The schema-only invariant (YAML knows the
table; CSV knows its rows) is preserved.

The cascade-delete integration test was rewritten to assert the
CSVs vanish; two new tests pin the rule (create -> no CSV;
delete --all-rows -> CSV removed).

Tests: 291 passing (256 lib + 9 + 9 + 17), 0 failing, 0 skipped.
2026-05-07 21:49:28 +00:00
claude@clouddev1 5c076f6d8f Iteration 2: per-command write-through to project.yaml, CSVs, history.log
Every successful user command now persists through to YAML, the
affected CSVs, and history.log inside the same SQLite transaction,
with the commit-db-last ordering from ADR-0015 §6: validate ->
mutate -> stage text + fsync -> atomic rename -> append history ->
commit. A failure in any text-write step rolls back the SQLite tx,
so disk state is unchanged on failure. Persistence failures are
routed through a new AppEvent::PersistenceFatal which sets a
fatal_message on the App, emits Action::Quit, and is printed to
stderr after terminal teardown so the banner remains above the
shell prompt (ADR-0015 §8).

New persistence module owns the file formats: hand-rolled YAML
schema writer, per-type CSV encoder (RFC 4180, NULL distinct from
empty string, base64 blobs), append-only history.log with ISO-8601
timestamps and successful-only entries. Atomic per-file writes via
tmp + fsync + rename.

The db worker holds an Option<Persistence>; tests still use
Database::open(":memory:") with no persistence. Action::ExecuteDsl
gains a source field carrying the user-typed text, threaded
through to history.log.

Tests: 289 passing (256 lib + 7 new integration + 9 lifecycle + 17
walking-skeleton), 0 failing, 0 skipped. Clippy clean with nursery
lints.
2026-05-07 21:09:15 +00:00
claude@clouddev1 601d3b6c51 Iteration 1: file-backed projects with auto-named temps, lock file, and L1 CLI
Replaces the in-memory database with an on-disk project. Startup either
opens a project at the positional CLI path (L1) or creates an auto-named
temp project (<YYYYMMDD>-<word>-<word>-<word>) under the OS-standard
data directory or a --data-dir override. The new project::Project type
owns the directory skeleton and a PID+hostname lock file with
stale-lock takeover via sysinfo. The status bar now shows
"Project: <Display Name>", derived by a small kebab/snake/camel
prettifier. Per-command persistence to YAML/CSV/history.log is NOT
yet wired -- that's Iteration 2; for now playground.db carries the
state across quits.

Tests: 257 passing (231 lib + 9 new integration + 17 existing),
0 failing, 0 skipped. Clippy clean with nursery lints.
2026-05-07 20:21:52 +00:00
claude@clouddev1 4fca862c6c Project storage runtime: ADR-0015 + ADR-0004/0007 amendments
Designs track-2 lifecycle and persistence end-to-end: per-command
write-through to db+yaml+csv+history.log gated by the combined db
persistence logic with commit-db-last ordering; existence-only load
with explicit rebuild command; --resume CLI flag backed by
<data-root>/last_project; in-TUI list-with-browse picker; lock file
for single-instance enforcement; fatal-banner-then-quit failure
model (with --resume making restart cheap); fatal CSV row-load
errors with full diagnosis; YYYYMMDD-word-word-word temp naming
with display-name prettifier; collision-checked names for both
temp and user-supplied projects. Project name lives only on the
filesystem (not duplicated in YAML). ADR-0004 and ADR-0007 amended
in place. requirements.md and CLAUDE.md updated; OOS-6 (global
rolling history) tracked as deferred.
2026-05-07 19:53:47 +00:00
claude@clouddev1 bfdf350ac8 Handoff doc + CLAUDE.md refresh for next session
- New docs/handoff/20260507-handoff-1.md captures session
  state, what's implemented, what's pending (ranked
  recommendations for next moves), sharp edges, and a
  smoke-test sequence.
- CLAUDE.md updated to reflect current reality: ADRs 0008-
  0014 added to the decisions-at-a-glance list, the
  "repository layout (planned)" placeholder replaced with
  the actual layout, key invariants spelled out, deferred
  list rebuilt from current requirements.md.
2026-05-07 18:07:18 +00:00
claude@clouddev1 305e5083d5 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.
2026-05-07 16:33:25 +00:00
claude@clouddev1 165068269b Foreign-key relationships, rebuild-table, polish round
DSL:
- add 1:n relationship [as <name>] from <P>.<col> to <C>.<col>
  [on delete <action>] [on update <action>] [--create-fk]
- drop relationship <name> | from <P>.<col> to <C>.<col>
- show table <name> for re-displaying a structure on demand

Database (ADR-0013):
- Rebuild-table primitive following SQLite's
  ALTER-via-rebuild recipe (foreign_keys=OFF outside tx,
  copy-by-name, foreign_key_check before commit). Reusable for
  B2 (column drops/renames/type changes).
- ReferentialAction enum (no action / restrict / set null /
  cascade); SET DEFAULT awaits column DEFAULTs.
- __rdbms_playground_relationships metadata table -- names,
  auto-generated as <Parent>_<pcol>_to_<Child>_<ccol>.
- Type::fk_target_type() validation at declaration; friendly
  errors for type mismatch, non-PK target, missing column,
  duplicate name.
- describe_table populates symmetric outbound + inbound
  relationship lists. drop_table refuses while inbound
  references exist; outbound metadata cleaned up alongside drop.

App / UI:
- In-line cursor editing in the input field: Left, Right,
  Home, End, Delete, Backspace honoring UTF-8 boundaries.
- PageUp / PageDown scrolls the output buffer; viewport row
  count fed back from the renderer via App::note_output_viewport
  so scroll is capped against the actual visible area
  (regression-tested) and snaps to the bottom on new output.
- Failure messages quote the command portion ("verb target"
  failed: ...) for visual clarity; RelationshipSelector has a
  proper Display impl so "no such relationship" reads cleanly.
- Structure rendering shows References / Referenced by sections.

Docs:
- ADR-0013 covers naming, metadata table, symmetric view, and
  the rebuild-table strategy.
- requirements.md updates: C3 (FK done), B2 (primitive in),
  T3 (compound-PK FK still pending). New entries: I1a (cursor
  editing -- landed), I1b (Ctrl-A/E and readline shortcuts --
  pending), V4 partial scroll, V5 (show family), C3a (modify
  relationship -- deferred).

Tests: 154 passing (140 lib + 14 integration), 0 skipped.
Clippy clean with nursery enabled.
2026-05-07 14:52:51 +00:00
claude@clouddev1 c1e52920eb DSL parser, async DB worker, types, history, metadata, polish
Track 1 implementation plus polish round.

Parser (chumsky):
- Grammar-based DSL producing a typed Command AST.
- create table X with pk [name:type[,name:type...]] supports
  arbitrary names, any user type, compound PKs natively. Bare
  form errors with a friendly hint pointing at `with pk`.
- add column to table X: Name (type); drop table X.
- Required clauses use keyword grammar; -- reserved for opt-in
  flags (ADR-0009). Custom Rich reasons preferred when surfacing
  chumsky errors so unknown-type messages list valid alternatives.

Database (ADR-0010, ADR-0012):
- rusqlite + STRICT tables + foreign_keys=ON.
- Dedicated worker thread; mpsc Request inbox, oneshot replies.
- Typed DbError with friendly_message() hook for H1.
- Internal __rdbms_playground_columns metadata table preserves
  user-facing types across schema reads, atomically maintained
  alongside DDL via Connection transactions. list_tables hides
  it via the new __rdbms_ internal-table convention.

Types (ADR-0005, ADR-0011):
- All ten user-facing types: text, int, real, decimal, bool,
  date, datetime, blob, serial, shortid.
- Type::fk_target_type() for FK-side column-type rule
  (Serial->Int, ShortId->Text, others identity) -- foundation
  for the FK iteration.

App / Runtime / UI:
- update() stays pure-sync; runtime dispatches DSL via spawned
  tasks, results post back as AppEvent::Dsl*.
- Items panel renders live tables list; output panel shows the
  user-facing structure of the current table after each DDL.
- In-memory command history (Up/Down, draft preservation,
  consecutive-duplicate dedup) -- I2 partial.
- Mouse capture removed; terminal native text selection
  restored (toggle approach revisited when scroll/click
  features land).

Docs:
- ADRs 0009 (DSL syntax conventions), 0010 (DB worker),
  0011 (FK type compat), 0012 (internal metadata table).
- requirements.md progress notes; new V4 entry for the
  scrollable session-log + inline rich rendering + Markdown
  export direction.

Tests: 103 passing (91 lib + 12 integration), 0 skipped.
Clippy clean with nursery enabled.
2026-05-07 13:32:19 +00:00
claude@clouddev1 25a0f1260f TUI walking skeleton (Phase 4)
First implementation milestone: Cargo project, dependencies,
and a minimal but functional TUI shell built on Ratatui +
Crossterm + Tokio in the Elm-style update/view pattern
(Candidate A from Phase 2/3 selection).

Includes:
- Three-region layout: items list (left), output + input + hint
  (right), bottom status bar with mode-aware shortcuts.
- Two themes (light, dark) plus COLORFGBG auto-detect, per
  NFR-7. CLI: --theme {light,dark}, --log-file <path>.
- Input modes per ADR-0003: simple (default), advanced, with
  the `:` one-shot escape including immediate prompt reaction
  ("Advanced:" label, advanced border) and auto-inserted space
  after a leading `:` in simple mode.
- App-level commands: `quit`/`q`, `mode simple`/`mode advanced`
  (canonical list per ADR-0003 — remaining commands land in
  later iterations).
- File logging via tracing, defaulting to ~/.rdbms-playground/
  playground.log so the TUI is not corrupted by stdio.

Testing per ADR-0008:
- Tier 1: 29 unit tests covering input handling, mode switch,
  one-shot escape, auto-space, output buffering, CLI parsing.
- Tier 2: 4 insta snapshots (default simple/advanced/light,
  one-shot active) of TestBackend frames.
- Tier 3: 7 integration tests driving synthetic events through
  App::update + render path.

All green: 36 tests, 0 failures, 0 skips. Clippy clean with
nursery lints enabled.
2026-05-07 11:17:58 +00:00
claude@clouddev1 aebfc7dcba Add Phase 1 requirements checklist with NFRs
Consolidates the conversation and ADRs into a checkable
Phase 1 requirements document: 66 functional items, 7 NFRs
(performance, visual quality, light/dark background support,
cross-platform parity), and 3 explicitly out-of-scope items.
Stable per-item IDs, status legend, test-suite baseline, and
maintenance rules.
2026-05-07 10:15:49 +00:00
claude@clouddev1 3a0c03d781 Initial planning docs: CLAUDE.md and ADRs 0000-0008
Captures up-front design decisions for RDBMS Playground:
stack (Rust + Ratatui + SQLite), input modes, project file
format, type vocabulary, undo snapshots and replay log,
sharing/export, and testing approach. ADR-0000 establishes
the ADR practice itself and mandates index upkeep alongside
any ADR change.
2026-05-07 09:27:31 +00:00