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