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.
This commit is contained in:
@@ -0,0 +1,81 @@
|
|||||||
|
# RDBMS Playground — project notes for Claude
|
||||||
|
|
||||||
|
## What this project is
|
||||||
|
|
||||||
|
A cross-platform TUI application that gives learners a sandbox for
|
||||||
|
exploring relational database concepts: tables, columns, primary
|
||||||
|
and foreign keys, relationships, indexes, queries, and query
|
||||||
|
plans. The audience is students from beginners to those ready for
|
||||||
|
raw SQL, and the design accommodates both ends of that spectrum.
|
||||||
|
|
||||||
|
The application is a teaching tool, not a database administration
|
||||||
|
tool. Decisions about the type system, command surface, and
|
||||||
|
backend choices are skewed toward pedagogy over breadth.
|
||||||
|
|
||||||
|
## Authoritative decisions
|
||||||
|
|
||||||
|
All significant design decisions live in `docs/adr/`. Read
|
||||||
|
`docs/adr/README.md` for the index. **Before proposing changes
|
||||||
|
that touch a decided area, read the relevant ADR.** Decisions are
|
||||||
|
not re-litigated casually — if a decision needs to change, write a
|
||||||
|
new ADR that supersedes the old one.
|
||||||
|
|
||||||
|
Current decisions at a glance (each backed by an ADR):
|
||||||
|
|
||||||
|
- **Stack:** Rust + Ratatui + Crossterm; `sqlparser-rs` for SQL,
|
||||||
|
`rusqlite` for the database (ADR-0001).
|
||||||
|
- **Backend:** SQLite with `STRICT` tables and FK enforcement on
|
||||||
|
(ADR-0002).
|
||||||
|
- **Input:** simple mode (DSL only) by default; advanced mode
|
||||||
|
(SQL + app-level commands) on toggle; `:` one-shot escape from
|
||||||
|
simple to advanced (ADR-0003). No other sigils.
|
||||||
|
- **Project format:** `project.yaml` + `data/<table>.csv` +
|
||||||
|
`history.log`; `playground.db` is a derived artifact (ADR-0004).
|
||||||
|
- **Types:** `text`, `int`, `real`, `decimal`, `bool`, `date`,
|
||||||
|
`datetime`, `blob`, `serial`, `shortid`. Compound primary keys
|
||||||
|
supported. No real UUIDs (ADR-0005).
|
||||||
|
- **Safety:** auto-snapshot before destructive ops; `:undo`;
|
||||||
|
append-only `history.log` for replay and scripting (ADR-0006).
|
||||||
|
- **Sharing:** `export` command produces a zip without the `.db`;
|
||||||
|
no hosted publishing (ADR-0007).
|
||||||
|
|
||||||
|
## Repository layout (planned)
|
||||||
|
|
||||||
|
The repository is in early planning. Once code lands, layout
|
||||||
|
conventions will be added here. For now, `docs/adr/` is the
|
||||||
|
canonical source of decided ground.
|
||||||
|
|
||||||
|
## Working style for this project
|
||||||
|
|
||||||
|
- **Documentation discipline.** Significant decisions get an ADR.
|
||||||
|
In-flight discussion stays in conversation or issues until it
|
||||||
|
settles.
|
||||||
|
- **Testing.** Per the user's global standards, tests are
|
||||||
|
established before changes, bugs are reproduced with failing
|
||||||
|
tests before being fixed, and "all green, no skips" is the only
|
||||||
|
acceptable end state. Integration tests exercise full flows
|
||||||
|
(e.g. project save → reload → rebuild produces the same
|
||||||
|
observable database), not just units in isolation.
|
||||||
|
- **No silent feature loss.** Anything in an ADR is decided. If
|
||||||
|
implementation reveals that a decision is wrong or impractical,
|
||||||
|
raise it explicitly and update the ADR — do not quietly drift.
|
||||||
|
- **Pedagogy wins ties.** When a design choice trades clarity for
|
||||||
|
raw capability, prefer clarity. Real RDBMS power-user features
|
||||||
|
exist; this app is not the place to teach them.
|
||||||
|
|
||||||
|
## Things deliberately deferred
|
||||||
|
|
||||||
|
These have been discussed but not yet decided. Do not implement
|
||||||
|
ahead of an ADR for any of them:
|
||||||
|
|
||||||
|
- Tutorial / lesson system architecture (acknowledged as in scope
|
||||||
|
for design; needs its own ADR before code).
|
||||||
|
- Query plan rendering specifics (the *what* is decided — annotated
|
||||||
|
tree view of `EXPLAIN QUERY PLAN`; the *how* is not).
|
||||||
|
- Friendly error message rewriting layer (decided to exist; design
|
||||||
|
not done).
|
||||||
|
- Sample data generation rules, including FK-aware generation for
|
||||||
|
junction tables.
|
||||||
|
- Visual ER diagram export.
|
||||||
|
- DSL grammar specifics (compound key syntax, m:n relationship
|
||||||
|
command, etc.).
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# ADR-0000: Record architecture decisions
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The RDBMS Playground project will accumulate design decisions as it
|
||||||
|
grows. We want those decisions to be traceable, reviewable, and easy
|
||||||
|
to challenge later when context has changed.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Record significant architecture and product decisions as Architecture
|
||||||
|
Decision Records (ADRs) in `docs/adr/`, using the Michael Nygard
|
||||||
|
format (Status, Context, Decision, Consequences). Files are numbered
|
||||||
|
sequentially with a four-digit prefix and a kebab-case slug, e.g.
|
||||||
|
`0001-language-and-tui-framework.md`.
|
||||||
|
|
||||||
|
Each ADR captures one decision. Superseding a decision means adding a
|
||||||
|
new ADR that references the old one and marking the old one as
|
||||||
|
"Superseded by ADR-NNNN".
|
||||||
|
|
||||||
|
ADRs document decisions, not speculation. An idea under discussion
|
||||||
|
belongs in conversation, an issue, or a design note — not an ADR. An
|
||||||
|
ADR is written once a decision has actually been made.
|
||||||
|
|
||||||
|
## Index discipline
|
||||||
|
|
||||||
|
`docs/adr/README.md` contains the canonical index of all ADRs.
|
||||||
|
Whenever an ADR is added, renamed, or has its status changed (e.g.
|
||||||
|
"Superseded by ADR-NNNN"), the index MUST be updated in the same
|
||||||
|
change. An ADR change without a corresponding index update is
|
||||||
|
incomplete.
|
||||||
|
|
||||||
|
The index lists ADRs in numerical order. Each entry shows the
|
||||||
|
number, title, and — where relevant — status annotations such as
|
||||||
|
"Superseded by ADR-NNNN" or "Deprecated".
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- New significant decisions require an ADR before or alongside the
|
||||||
|
implementation that depends on them.
|
||||||
|
- Old decisions remain visible even after they are superseded.
|
||||||
|
- Reviewers can audit the rationale chain by reading `docs/adr/` in
|
||||||
|
order.
|
||||||
|
- The index in `README.md` stays trustworthy because keeping it
|
||||||
|
current is part of every ADR change, not an afterthought.
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# ADR-0001: Language and TUI framework
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
RDBMS Playground is a cross-platform terminal application aimed at
|
||||||
|
learners. It needs to feel fast, polished, and colourful, install
|
||||||
|
cleanly on Linux, macOS, and Windows, and ship as a single binary
|
||||||
|
with no runtime dependencies.
|
||||||
|
|
||||||
|
Beyond TUI rendering, the application has substantial SQL-handling
|
||||||
|
needs: syntax highlighting, parsing user input to distinguish app
|
||||||
|
DSL commands from SQL, rewriting simplified types into backend
|
||||||
|
types, and analysing query plans for the teaching features.
|
||||||
|
|
||||||
|
Two stacks were realistic candidates:
|
||||||
|
|
||||||
|
1. **Rust + Ratatui + Crossterm** — strong cross-platform terminal
|
||||||
|
support, single static binary, mature ecosystem. Crucially,
|
||||||
|
`sqlparser-rs` is a high-quality dialect-aware SQL parser
|
||||||
|
directly applicable to the parsing, highlighting, and query
|
||||||
|
analysis features.
|
||||||
|
2. **Go + Bubble Tea (Charm)** — excellent default aesthetics,
|
||||||
|
single static binary, easy distribution. Lacks an equivalent to
|
||||||
|
`sqlparser-rs`; SQL parsing would need to be written from
|
||||||
|
scratch or wrap a less suitable library.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Use **Rust with Ratatui and Crossterm** for the TUI, with
|
||||||
|
`sqlparser-rs` for SQL parsing and `rusqlite` for the database
|
||||||
|
layer. Distribute as prebuilt binaries via GitHub releases plus
|
||||||
|
package managers (`cargo binstall`, Homebrew, Scoop, `winget`).
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- Single static binary on all three target platforms.
|
||||||
|
- Strong fit between SQL-heavy features and the Rust SQL ecosystem
|
||||||
|
(`sqlparser-rs`, `rusqlite`).
|
||||||
|
- Slightly steeper contributor on-ramp for developers unfamiliar
|
||||||
|
with Rust compared to Go.
|
||||||
|
- TUI styling will require explicit work to match the polish that
|
||||||
|
Bubble Tea / Lipgloss give for free; budget for it in the design
|
||||||
|
pass.
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# ADR-0002: Database engine
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The application teaches relational database concepts. Requirements
|
||||||
|
on the engine:
|
||||||
|
|
||||||
|
- File-based, with a single binary file that can live in the
|
||||||
|
project folder.
|
||||||
|
- Real RDBMS feature set: typed columns, primary keys, foreign keys
|
||||||
|
with referential integrity, indexes, query planner output we can
|
||||||
|
expose to learners.
|
||||||
|
- Industry adoption — students should leave with a skill that maps
|
||||||
|
to something they will encounter again.
|
||||||
|
- Embeddable from Rust without arcane build steps.
|
||||||
|
|
||||||
|
Candidates considered:
|
||||||
|
|
||||||
|
- **SQLite** — file-based, near-universal adoption, supports FK
|
||||||
|
enforcement, has `EXPLAIN QUERY PLAN`, and since 3.37 supports
|
||||||
|
`STRICT` tables which give proper type enforcement instead of
|
||||||
|
SQLite's traditional loose type affinity.
|
||||||
|
- **DuckDB** — file-based, modern, but analytical/columnar by
|
||||||
|
design; would teach analytical instincts that mislead in
|
||||||
|
transactional contexts.
|
||||||
|
- **Embedded Postgres (pglite, pg_embed)** — closest to a "real"
|
||||||
|
RDBMS, but adds friction and complicates packaging.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Use **SQLite** via the `rusqlite` crate. All tables are created as
|
||||||
|
`STRICT` tables. Foreign-key enforcement is enabled per-connection
|
||||||
|
(`PRAGMA foreign_keys = ON`).
|
||||||
|
|
||||||
|
Simplified user-facing column types (see ADR-0005) are mapped to
|
||||||
|
the underlying SQLite STRICT types at parse time.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- Tight, low-friction packaging — `rusqlite` bundles SQLite.
|
||||||
|
- `EXPLAIN QUERY PLAN` output is directly usable for the query
|
||||||
|
analysis feature (ADR pending).
|
||||||
|
- SQLite's `ALTER TABLE` is limited (e.g. type changes, some
|
||||||
|
drops). Schema evolution will use the rebuild-table technique
|
||||||
|
internally; this is hidden from users in simple mode and exposed
|
||||||
|
honestly in advanced mode.
|
||||||
|
- STRICT tables forbid the historical permissive typing students
|
||||||
|
might encounter elsewhere — this is intentional and matches the
|
||||||
|
pedagogical goal.
|
||||||
|
- Booleans are stored as `INTEGER` (0/1) per SQLite's STRICT type
|
||||||
|
set; the `bool` user-facing type maps to this and is rendered as
|
||||||
|
`true`/`false` in result views.
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
# ADR-0003: Input modes and command dispatch
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The input field accepts three logically distinct kinds of input:
|
||||||
|
|
||||||
|
1. **App DSL data commands** — simplified, space-separated forms
|
||||||
|
like `create table Customers` or
|
||||||
|
`add column to table Customers: Name (string)`. These are the
|
||||||
|
primary teaching surface for beginners.
|
||||||
|
2. **App-level commands** — operations on the application itself
|
||||||
|
rather than the data: `save`, `load`, `export`, `quit`,
|
||||||
|
`undo`, `seed`, etc. These should always be available
|
||||||
|
regardless of mode.
|
||||||
|
3. **Raw SQL** — for users ready to type SQL directly, including
|
||||||
|
DDL and queries.
|
||||||
|
|
||||||
|
Beginners should not be exposed to SQL by accident, but advanced
|
||||||
|
users should not be punished for wanting to drive the app entirely
|
||||||
|
from the keyboard. Auto-detecting "is this SQL?" is unreliable
|
||||||
|
because the same first words (`create table …`) appear in both the
|
||||||
|
DSL and SQL.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
The input field has two modes, switchable by the user:
|
||||||
|
|
||||||
|
- **Simple mode** (default).
|
||||||
|
- Accepts app DSL data commands and app-level commands.
|
||||||
|
- Raw SQL is not executed; instead the input is recognised as
|
||||||
|
SQL and a friendly hint suggests switching to advanced mode or
|
||||||
|
using the one-shot escape (below).
|
||||||
|
- The mode is indicated by a label above the input field
|
||||||
|
(e.g. `Simple`).
|
||||||
|
|
||||||
|
- **Advanced mode**.
|
||||||
|
- Accepts raw SQL by default.
|
||||||
|
- App-level commands (the canonical list below) are still
|
||||||
|
recognised without any prefix — forcing users to switch back
|
||||||
|
to simple mode for these would be user-hostile, and advanced
|
||||||
|
users are exactly the ones most likely to live in
|
||||||
|
command-driven workflows.
|
||||||
|
- Mode is indicated by a label and a distinct input border
|
||||||
|
colour to make the elevated mode visually obvious.
|
||||||
|
|
||||||
|
- **One-shot escape with `:` in simple mode.**
|
||||||
|
- Prefixing a single line with `:` interprets it as advanced
|
||||||
|
input for that submission only. The prompt label changes
|
||||||
|
(e.g. `Advanced:`) while the prefix is present.
|
||||||
|
- This is the only sigil in the system. App-level commands
|
||||||
|
never use a sigil.
|
||||||
|
|
||||||
|
The set of "always available" app-level commands is enumerated
|
||||||
|
explicitly; it is not heuristic. The initial canonical list is:
|
||||||
|
|
||||||
|
| Command | Effect |
|
||||||
|
|-----------|---------------------------------------------------------|
|
||||||
|
| `save` | Persist the project to its current location. |
|
||||||
|
| `save as` | Persist the project to a chosen name and location. |
|
||||||
|
| `load` | Open the load dialog (project picker). |
|
||||||
|
| `new` | Start a new auto-named temporary project. |
|
||||||
|
| `export` | Produce a shareable zip (ADR-0007). |
|
||||||
|
| `import` | Open an exported project zip. |
|
||||||
|
| `seed` | Generate sample data (rules per a future ADR). |
|
||||||
|
| `replay` | Replay a `history.log` or `.commands` script. |
|
||||||
|
| `undo` | Restore the most recent snapshot (ADR-0006). |
|
||||||
|
| `redo` | Re-apply an undone state (ADR-0006). |
|
||||||
|
| `mode` | Switch between simple and advanced (`mode simple`/`mode advanced`). |
|
||||||
|
| `help` | Show contextual help. |
|
||||||
|
| `hint` | Request a hint for the current input (ADR pending). |
|
||||||
|
| `quit` | Exit the application. |
|
||||||
|
|
||||||
|
This list is **definitive** and applies in both modes. Adding,
|
||||||
|
removing, or renaming a command requires either:
|
||||||
|
|
||||||
|
- **Before code exists:** an amendment to this ADR.
|
||||||
|
- **Once code exists:** the in-code command registry becomes the
|
||||||
|
source of truth; this ADR is updated to point at the registry,
|
||||||
|
and any change to the registry's public surface still requires
|
||||||
|
an ADR note explaining the change.
|
||||||
|
|
||||||
|
Commands that operate on the data (e.g. `create table`,
|
||||||
|
`add column`, `create m:n relationship`) are **not** in this list
|
||||||
|
— they are DSL data commands and behave per the simple/advanced
|
||||||
|
mode rules above.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- Parsing must distinguish three categories — app-level command,
|
||||||
|
DSL data command, or SQL — and the mode determines what happens
|
||||||
|
if the input does not match the first two.
|
||||||
|
- Mode state is per-input-session and persists across submissions
|
||||||
|
until changed.
|
||||||
|
- Visual distinction (label, border colour) is required for safety:
|
||||||
|
users must always know whether their next submission will be
|
||||||
|
treated as SQL.
|
||||||
|
- The `:` escape lets advanced users in simple mode run a quick
|
||||||
|
query without flipping modes — useful during teaching when an
|
||||||
|
instructor wants to demonstrate SQL once.
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# ADR-0004: Project file format
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Projects must be:
|
||||||
|
|
||||||
|
- Shareable — students and instructors should be able to send
|
||||||
|
projects to each other and reconstruct the full database state.
|
||||||
|
- Diffable — version control should produce meaningful diffs as a
|
||||||
|
schema or data set evolves.
|
||||||
|
- Versioned — the format will change as the app evolves, and old
|
||||||
|
projects must continue to load.
|
||||||
|
- Efficient enough for moderate amounts of practice data without
|
||||||
|
forcing users into pathological YAML files of tens of thousands
|
||||||
|
of rows.
|
||||||
|
|
||||||
|
The on-disk SQLite file (`.db`) is convenient but binary and not
|
||||||
|
suited to sharing or diffing.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
A project is a directory containing:
|
||||||
|
|
||||||
|
```
|
||||||
|
<project-name>/
|
||||||
|
project.yaml # schema, relationships, metadata, version
|
||||||
|
data/
|
||||||
|
<table>.csv # one CSV file per table, with header row
|
||||||
|
playground.db # derived; rebuildable from project.yaml + data/
|
||||||
|
history.log # append-only command/replay log (see ADR-0006)
|
||||||
|
```
|
||||||
|
|
||||||
|
- `project.yaml` carries a top-level `version: 1` field from the
|
||||||
|
outset, plus all schema, relationship, and project metadata.
|
||||||
|
- Table data lives in `data/<table>.csv` (UTF-8, header row, RFC
|
||||||
|
4180 quoting). One file per table keeps diffs scoped and avoids
|
||||||
|
monolithic YAML.
|
||||||
|
- `playground.db` is a **derived artifact**. The authoritative
|
||||||
|
state is `project.yaml` + `data/`. The `.db` file is kept when
|
||||||
|
present (we never silently drop it) but can be rebuilt from the
|
||||||
|
text sources at any time.
|
||||||
|
- Rebuilding when no `.db` exists: silent, automatic.
|
||||||
|
- Rebuilding when a `.db` exists: requires user confirmation
|
||||||
|
with a summary diff (e.g. "3 tables, 47 rows will be
|
||||||
|
recreated; existing `.db` will be replaced").
|
||||||
|
- A `.gitignore` template is created in each project; by default
|
||||||
|
the `.db` file is ignored so version control captures only the
|
||||||
|
authoritative sources.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- Projects round-trip cleanly through git, email, and zip.
|
||||||
|
- Large practice data sets remain efficient (CSV is appropriate).
|
||||||
|
- Schema review remains pleasant (YAML is appropriate).
|
||||||
|
- The app must be able to (re)build a database from the text
|
||||||
|
sources at any time — this is a first-class code path, not an
|
||||||
|
edge case.
|
||||||
|
- The `version` field opens the door to format migrations as the
|
||||||
|
app evolves; old projects load by running registered migrators
|
||||||
|
in sequence.
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
# ADR-0005: Column type vocabulary
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Real RDBMS engines expose many type variants that exist for
|
||||||
|
historical, performance, or platform reasons. A learner does not
|
||||||
|
benefit from picking between `VARCHAR(255)`, `TEXT`, `CHAR(40)`,
|
||||||
|
and `CLOB`. We control the user-facing surface and can present a
|
||||||
|
small, semantically clear set of types that maps cleanly to the
|
||||||
|
chosen backend (SQLite STRICT, ADR-0002).
|
||||||
|
|
||||||
|
We also want to teach two distinct lessons about identifiers:
|
||||||
|
|
||||||
|
1. The default, easiest path: a simple auto-incrementing integer
|
||||||
|
primary key. Used in 90% of intro examples.
|
||||||
|
2. Why integers aren't always the right answer: short random
|
||||||
|
identifiers that survive merging data sets, sharing, or
|
||||||
|
migration without collisions.
|
||||||
|
|
||||||
|
Real UUIDs (36 characters) are too wide to display comfortably in
|
||||||
|
TUI columns and exceed what learners actually need to understand
|
||||||
|
the concept.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
The user-facing column type vocabulary is:
|
||||||
|
|
||||||
|
| User-facing type | SQLite STRICT mapping | Notes |
|
||||||
|
|------------------|-----------------------|-------------------------------------------|
|
||||||
|
| `text` | `TEXT` | Strings of any length. |
|
||||||
|
| `int` | `INTEGER` | Plain integer. |
|
||||||
|
| `real` | `REAL` | IEEE-754 double. |
|
||||||
|
| `decimal` | `TEXT` | Stored as decimal string; rendered numeric. |
|
||||||
|
| `bool` | `INTEGER` | 0/1 internally; `true`/`false` rendered. |
|
||||||
|
| `date` | `TEXT` | ISO 8601 (`YYYY-MM-DD`). |
|
||||||
|
| `datetime` | `TEXT` | ISO 8601 (`YYYY-MM-DDTHH:MM:SS[.fff][Z]`).|
|
||||||
|
| `blob` | `BLOB` | Binary data. |
|
||||||
|
| `serial` | `INTEGER PK AUTOINC.` | Auto-incrementing integer; PK by default. |
|
||||||
|
| `shortid` | `TEXT` | 10–12 char base58 random; PK by default. |
|
||||||
|
|
||||||
|
`shortid` uses base58 (no ambiguous `0`/`O`/`I`/`l`) and is
|
||||||
|
generated client-side at insert time when the column has no value
|
||||||
|
supplied.
|
||||||
|
|
||||||
|
Decimal is stored as text to preserve precision — applications
|
||||||
|
that need numeric comparison must use the engine's casts; this is
|
||||||
|
acceptable for a teaching context and the constraint is documented.
|
||||||
|
|
||||||
|
**Compound primary keys are supported.** They are essential for
|
||||||
|
junction tables in m:n relationships (e.g. `OrderLines` keyed on
|
||||||
|
`(order_id, product_id)`) and skipping them would teach the wrong
|
||||||
|
lesson. The simplified DSL provides natural syntax for them
|
||||||
|
(specifics in a later ADR).
|
||||||
|
|
||||||
|
True UUIDs are intentionally **not** in the type set.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- The type system is small enough to teach in five minutes.
|
||||||
|
- Mapping to SQLite STRICT is mechanical and lossless for the
|
||||||
|
intended use cases.
|
||||||
|
- The shortid generator is a small, well-tested utility — bounded
|
||||||
|
scope, no third-party dependency required.
|
||||||
|
- Junction tables and other compound-key scenarios are
|
||||||
|
first-class, reinforcing relational fundamentals.
|
||||||
|
- Learners who later need a true UUID column will find that the
|
||||||
|
app does not provide one; this is a deliberate trade-off in
|
||||||
|
favour of TUI legibility.
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# ADR-0006: Undo snapshots and replay log
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Two related features address the same underlying need — making the
|
||||||
|
application safe and reproducible for learners:
|
||||||
|
|
||||||
|
1. **Accidental destruction.** A student typing `DROP TABLE
|
||||||
|
Customers` and then realising what they did is a near-certain
|
||||||
|
event in this audience. Without a recovery path, the experience
|
||||||
|
is hostile and the learning moment is lost to panic.
|
||||||
|
2. **Replay and scripting.** A persistent record of every
|
||||||
|
executed command is useful for tutorials, debugging, sharing
|
||||||
|
reproducible problem reports, and rebuilding a project from a
|
||||||
|
blank slate.
|
||||||
|
|
||||||
|
Both features are cheap to implement and high-leverage.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
### Undo snapshots
|
||||||
|
|
||||||
|
Before any destructive operation — `DROP`, `DELETE`, `TRUNCATE`,
|
||||||
|
schema-rebuild migrations, restore, etc. — the application takes a
|
||||||
|
snapshot of the database using SQLite's online backup API into a
|
||||||
|
ring buffer of recent snapshots (size to be tuned; initial target
|
||||||
|
N = 10).
|
||||||
|
|
||||||
|
An `undo` command (available in both modes as an app-level
|
||||||
|
command per ADR-0003 — no sigil) restores the most recent
|
||||||
|
snapshot. Each undo step is itself snapshotted to keep `redo`
|
||||||
|
possible.
|
||||||
|
|
||||||
|
**Undo requires confirmation.** Snapshots are taken only before
|
||||||
|
destructive operations, so the "current" state may include
|
||||||
|
non-destructive work (inserts, updates, schema additions) done
|
||||||
|
since the last snapshot. Restoring a snapshot therefore *can*
|
||||||
|
discard data the user has not been explicitly warned about.
|
||||||
|
|
||||||
|
Before restoring, the application displays a confirmation prompt
|
||||||
|
that includes:
|
||||||
|
|
||||||
|
- The snapshot's timestamp (local time, with a relative form such
|
||||||
|
as "12 minutes ago").
|
||||||
|
- A short summary of the operation that triggered the snapshot
|
||||||
|
(the command text, e.g. `DROP TABLE Customers`).
|
||||||
|
- A summary of changes that will be discarded if the undo
|
||||||
|
proceeds — at minimum, counts of rows added/modified/deleted
|
||||||
|
per table and any schema changes since the snapshot.
|
||||||
|
|
||||||
|
The user must explicitly confirm. A keyboard shortcut for
|
||||||
|
"confirm" is provided so power users are not slowed down, but
|
||||||
|
there is no flag to suppress the prompt — undo is rare enough,
|
||||||
|
and consequential enough, that the prompt is always shown.
|
||||||
|
|
||||||
|
### Replay log (`history.log`)
|
||||||
|
|
||||||
|
Every successfully executed command — DSL or SQL — is appended to
|
||||||
|
`history.log` in the project directory, one command per record,
|
||||||
|
with a timestamp and the resulting status. The format is
|
||||||
|
deliberately simple and human-readable so it can be hand-edited
|
||||||
|
and replayed.
|
||||||
|
|
||||||
|
The same format serves three purposes:
|
||||||
|
|
||||||
|
- A persistent input history surfaced via the TUI history feature.
|
||||||
|
- A scripting format: `.commands` files (or `history.log` itself)
|
||||||
|
can be replayed via a `replay` command.
|
||||||
|
- A reproducible bug-report artifact when a project is shared.
|
||||||
|
|
||||||
|
The log is append-only during a session. It is **not** the
|
||||||
|
authoritative state of the project (that lives in `project.yaml`
|
||||||
|
+ `data/`, ADR-0004) — it is an audit and replay trail.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- Snapshots add modest overhead per destructive operation. The
|
||||||
|
cost is bounded; learners care about safety far more than
|
||||||
|
microseconds.
|
||||||
|
- The ring buffer size must be tuned later based on realistic
|
||||||
|
database sizes; an initial value is fine for now.
|
||||||
|
- The replay log enables a future `replay` / scripting feature
|
||||||
|
with no additional storage commitment.
|
||||||
|
- Tutorial authors gain a natural "starter script" format for
|
||||||
|
exercises.
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# ADR-0007: Sharing and export
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Learners and instructors will want to share projects: an
|
||||||
|
instructor distributing an exercise, a student attaching a
|
||||||
|
project to a question, a collaborator sending a snapshot.
|
||||||
|
|
||||||
|
The need is real, but committing to a hosted "publish" feature
|
||||||
|
(account system, server, namespace, moderation) would dwarf the
|
||||||
|
core app and is not warranted for a teaching tool. We can satisfy
|
||||||
|
the need entirely with local artifacts and documentation.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
No hosted publishing feature. Sharing is supported via two
|
||||||
|
mechanisms:
|
||||||
|
|
||||||
|
1. **`export` command.**
|
||||||
|
- Available as an app-level command in both input modes
|
||||||
|
(ADR-0003).
|
||||||
|
- Produces a single zip file containing `project.yaml`,
|
||||||
|
`data/`, and `history.log`, **excluding** `playground.db`
|
||||||
|
(the recipient rebuilds it on open per ADR-0004).
|
||||||
|
- Default output path is the parent directory of the project.
|
||||||
|
- Default filename:
|
||||||
|
`YYYYMMDD-<projectname>-export-<sequence>.zip`, where
|
||||||
|
`YYYYMMDD` is today's local date and `<sequence>` is a
|
||||||
|
two-digit zero-padded counter that starts at `01` and is
|
||||||
|
incremented to avoid clobbering an existing file in the
|
||||||
|
output directory on the same day.
|
||||||
|
- An explicit path and/or filename may be given to override
|
||||||
|
the default.
|
||||||
|
|
||||||
|
2. **Documented recipes.**
|
||||||
|
- Each new project includes a `.gitignore` template that
|
||||||
|
excludes `playground.db`, so committing a project to git
|
||||||
|
yields a clean, diff-friendly history.
|
||||||
|
- The user-facing documentation includes recipes for sharing
|
||||||
|
via git, email, and direct file transfer.
|
||||||
|
|
||||||
|
If real-world usage later reveals friction these mechanisms cannot
|
||||||
|
solve, a publish feature can be revisited as a separate ADR.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- Zero server-side surface area, zero accounts, zero hosting
|
||||||
|
costs.
|
||||||
|
- Sharing is a power-user-friendly workflow from day one without
|
||||||
|
blocking on infrastructure decisions.
|
||||||
|
- Recipients reconstruct the database deterministically from the
|
||||||
|
authoritative text sources — sharing is automatically a
|
||||||
|
validation of the rebuild path.
|
||||||
|
- If a future hosted feature is ever added, this ADR is
|
||||||
|
superseded, not extended.
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
# ADR-0008: Testing approach
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The project's working standards (`CLAUDE.md`, plus the user's
|
||||||
|
global testing rules) require:
|
||||||
|
|
||||||
|
- Test coverage established before changes.
|
||||||
|
- Bugs reproduced with failing tests before fixes.
|
||||||
|
- Integration tests that exercise the full stack a user touches.
|
||||||
|
- "All green, zero skips" as the only acceptable end state.
|
||||||
|
|
||||||
|
A TUI application needs a credible testing story for those rules
|
||||||
|
to be enforceable. Naïvely, "TUI testing" sounds like an
|
||||||
|
afterthought of manual screenshots. In practice, the Rust +
|
||||||
|
Ratatui ecosystem supports automation across every level that
|
||||||
|
matters, and we want to commit to that explicitly rather than
|
||||||
|
discover it ad-hoc.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Testing is structured in four tiers. Tiers 1–3 run on every
|
||||||
|
commit in CI; tier 4 runs on every commit for a focused subset of
|
||||||
|
critical flows and on a nightly schedule for broader coverage.
|
||||||
|
|
||||||
|
### Tier 1 — Pure-logic unit tests
|
||||||
|
|
||||||
|
Standard `cargo test` against modules with no terminal
|
||||||
|
dependency:
|
||||||
|
|
||||||
|
- DSL parser and command dispatcher
|
||||||
|
- Type mapping (user-facing → SQLite STRICT types)
|
||||||
|
- Project I/O (`project.yaml` + `data/<table>.csv` round-tripping)
|
||||||
|
- Database rebuild from authoritative text sources
|
||||||
|
- Snapshot ring buffer logic (ADR-0006)
|
||||||
|
- Replay log writer/reader
|
||||||
|
|
||||||
|
These are the bulk of the test count. Every behavioural unit has
|
||||||
|
unit tests; modules with non-trivial logic also have property
|
||||||
|
tests via `proptest` where the input space justifies it (parser
|
||||||
|
inputs, type coercion, CSV escaping, etc.).
|
||||||
|
|
||||||
|
### Tier 2 — Render assertions via Ratatui `TestBackend`
|
||||||
|
|
||||||
|
Ratatui's in-tree `TestBackend` renders into a `Buffer` (a 2D
|
||||||
|
cell grid). Tests build an app state, render a frame, and assert
|
||||||
|
on the resulting buffer.
|
||||||
|
|
||||||
|
- **Cell-level assertions** for narrow tests
|
||||||
|
("the status bar shows mode label `Simple` in the expected
|
||||||
|
style").
|
||||||
|
- **Snapshot tests** via `insta` for whole-frame coverage of
|
||||||
|
representative views (default screen, query result table,
|
||||||
|
schema view, undo confirmation prompt). Snapshots are checked
|
||||||
|
in and reviewed on diff.
|
||||||
|
|
||||||
|
Snapshot discipline:
|
||||||
|
|
||||||
|
- Snapshots cover stable, intentional UI surfaces; they are not
|
||||||
|
added reflexively to every component.
|
||||||
|
- A snapshot diff is treated as a real review item, not a
|
||||||
|
rubber-stamp. Reviewers must confirm the change is intended.
|
||||||
|
|
||||||
|
### Tier 3 — Synthetic event-loop integration tests
|
||||||
|
|
||||||
|
The application's update function consumes
|
||||||
|
`crossterm::event::Event` values. Tests feed sequences of
|
||||||
|
synthetic events (`KeyEvent`, `MouseEvent`, resize) to the update
|
||||||
|
function, then render via `TestBackend` and assert on both
|
||||||
|
state and buffer.
|
||||||
|
|
||||||
|
This tier is the equivalent of `react-testing-library` for our
|
||||||
|
TUI: it exercises the full input → state → render path without a
|
||||||
|
real terminal, and is where the most valuable behavioural tests
|
||||||
|
live. Examples:
|
||||||
|
|
||||||
|
- Typing a DSL command in simple mode, submitting with
|
||||||
|
Ctrl-Enter, asserting the table list updates and the schema
|
||||||
|
view re-renders.
|
||||||
|
- Triggering `undo`, asserting the confirmation prompt appears
|
||||||
|
with the expected timestamp and change summary, confirming,
|
||||||
|
asserting state restoration.
|
||||||
|
- Switching modes and verifying the prompt label and border
|
||||||
|
colour change.
|
||||||
|
|
||||||
|
### Tier 4 — PTY-based black-box end-to-end
|
||||||
|
|
||||||
|
A small number of critical flows are exercised against the
|
||||||
|
**actual built binary** in a pseudo-terminal:
|
||||||
|
|
||||||
|
- Tooling: `portable-pty` for the PTY, `expectrl` for
|
||||||
|
expect-style scripting, `vt100` to parse the terminal output
|
||||||
|
stream into an inspectable cell grid.
|
||||||
|
- These tests catch issues the lower tiers miss: TTY setup,
|
||||||
|
signal handling, terminal mode transitions, real I/O timing.
|
||||||
|
|
||||||
|
Tier 4 is **reserved for the highest-value flows**, not blanket
|
||||||
|
coverage. The initial scope is:
|
||||||
|
|
||||||
|
- Cold launch → first DDL command → graceful quit.
|
||||||
|
- Project save → process restart → reopen → identical state.
|
||||||
|
- Project export → import in a fresh project → rebuilt database
|
||||||
|
matches the source.
|
||||||
|
- `undo` immediately after a `DROP TABLE`, including the
|
||||||
|
confirmation prompt.
|
||||||
|
|
||||||
|
Tier 4 tests run in CI on every commit (the focused list above)
|
||||||
|
and on a nightly schedule for any extended coverage.
|
||||||
|
|
||||||
|
## Tooling commitments
|
||||||
|
|
||||||
|
- `cargo test` — Tier 1, Tier 2, Tier 3.
|
||||||
|
- `proptest` — property-based testing for parser and conversion
|
||||||
|
layers.
|
||||||
|
- Ratatui's `TestBackend` — frame rendering for tests.
|
||||||
|
- `insta` — snapshot testing of rendered buffers.
|
||||||
|
- `portable-pty`, `expectrl`, `vt100` — Tier 4 PTY-based tests.
|
||||||
|
- CI matrix covers Linux, macOS, and Windows on stable Rust.
|
||||||
|
|
||||||
|
## Honest limits
|
||||||
|
|
||||||
|
- **No cross-terminal-emulator regression coverage.** Tier 4
|
||||||
|
exercises a PTY but not real terminal emulators (xterm,
|
||||||
|
Alacritty, Windows Terminal, etc.). Crossterm abstracts these
|
||||||
|
well in practice; if a real-emulator regression ever surfaces,
|
||||||
|
we will revisit.
|
||||||
|
- **No visual aesthetic checks.** Tests assert cell contents and
|
||||||
|
styles, not "this layout is pretty". Visual polish is reviewed
|
||||||
|
manually by humans.
|
||||||
|
- **Snapshot brittleness** is a known failure mode. We mitigate
|
||||||
|
by being selective about what gets a snapshot and by treating
|
||||||
|
snapshot diffs as real review items.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- Test discipline from `CLAUDE.md` is enforceable on every
|
||||||
|
layer: parser bugs caught at Tier 1, UI flow bugs caught at
|
||||||
|
Tier 3, real-binary regressions caught at Tier 4.
|
||||||
|
- Module boundaries are designed for testability from the start
|
||||||
|
(pure logic modules separate from rendering, an explicit update
|
||||||
|
function consuming events).
|
||||||
|
- CI cost is real but bounded: Tiers 1–3 are fast; Tier 4 is the
|
||||||
|
only slow tier and is kept narrow.
|
||||||
|
- Adding a feature implies adding tests at the appropriate tier
|
||||||
|
(or tiers); coverage is not retrofitted later.
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# Architecture Decision Records
|
||||||
|
|
||||||
|
This directory contains the project's ADRs, recorded per
|
||||||
|
[ADR-0000](0000-record-architecture-decisions.md).
|
||||||
|
|
||||||
|
## Index
|
||||||
|
|
||||||
|
- [ADR-0000 — Record architecture decisions](0000-record-architecture-decisions.md)
|
||||||
|
- [ADR-0001 — Language and TUI framework](0001-language-and-tui-framework.md)
|
||||||
|
- [ADR-0002 — Database engine](0002-database-engine.md)
|
||||||
|
- [ADR-0003 — Input modes and command dispatch](0003-input-modes-and-command-dispatch.md)
|
||||||
|
- [ADR-0004 — Project file format](0004-project-file-format.md)
|
||||||
|
- [ADR-0005 — Column type vocabulary](0005-column-type-vocabulary.md)
|
||||||
|
- [ADR-0006 — Undo snapshots and replay log](0006-undo-snapshots-and-replay-log.md)
|
||||||
|
- [ADR-0007 — Sharing and export](0007-sharing-and-export.md)
|
||||||
|
- [ADR-0008 — Testing approach](0008-testing-approach.md)
|
||||||
Reference in New Issue
Block a user