# Session handoff — 2026-05-17 (15) Fifteenth handover. This session implemented **ADR-0025 (Indexes)** end to end — a fresh feature off the `docs/requirements.md` roadmap — and fixed an input-history navigation bug found while testing it. **Headline: indexes are a real DSL feature now.** `add index` / `drop index`, rebuild-safe, persisted in `project.yaml`, shown in the structure view and a new nested items panel. ADR-0025 is Accepted and fully implemented. The natural next job is **QA1 — `EXPLAIN QUERY PLAN`** — now unblocked because there is finally an index for a plan to use; see §4. ## State at handoff **Branch:** `main`. Working tree clean. `origin/main` is at `0dc159f` (the ADR-0025 commit — the user pushed it); local HEAD is **1 commit ahead** (`4e4dbbf`, the history fix — unpushed; the user pushes asynchronously). Commits since handoff-14: ``` 4e4dbbf Input history: reset the nav cursor on every submit 0dc159f Indexes: add index / drop index, persistence, display (ADR-0025) ``` **Tests:** **1039 passing, 0 failing, 1 ignored** (`cargo test` — up from 1006/1007). The ignored test is the long-standing `` ```ignore `` doc-test in `src/friendly/mod.rs`. The typing-surface matrix is now **152 cells** (was 144). **Clippy:** clean with `nursery` lints + `-D warnings`. ## §1. What shipped ### ADR-0025 — indexes (`0dc159f`) A new ADR (`docs/adr/0025-indexes.md`, Accepted) and its full implementation. The four design forks were settled by the user before drafting: - **Naming:** optional, `add index [as ] on ()`; auto-name `__idx` when omitted. Mirrors the ADR-0013 `as`-name convention. - **UNIQUE:** out of scope — plain non-unique indexes only; `UNIQUE` stays a separate `C3` sub-item. - **Items list (S2):** the left panel is now a nested tree — each table with its index names indented beneath it. - **`drop column`:** refuses an indexed column by default; a new `--cascade` flag opts into dropping the covering indexes. What landed: - **Grammar** (`src/dsl/grammar/ddl.rs`): `add index` is a third branch of `add`; `drop index` a fourth branch of `drop`; both drop forms (`drop index ` / `drop index on ()`). `drop column` gained an optional `--cascade` flag. - **AST** (`src/dsl/command.rs`): `Command::AddIndex` / `Command::DropIndex`, an `IndexSelector` enum (`Named` / `Columns`, mirroring `RelationshipSelector`), and a `cascade: bool` field on `Command::DropColumn`. - **db layer** (`src/db.rs`): `do_add_index` / `do_drop_index`; index reads via the engine's native catalog (`pragma_index_list` / `pragma_index_info`, origin `c` only) — **no metadata table**, unlike relationships; `IndexInfo` added to `TableDescription`; friendly errors for duplicate name, redundant column set, missing table/column, unknown / ambiguous index. - **Rebuild safety:** `rebuild_table_with_copy` now captures a table's indexes before the `DROP TABLE` and recreates them after the rename — so `change column` and the relationship operations no longer silently drop indexes. - **`drop column --cascade`:** `do_drop_column` detects covering indexes, refuses without the flag, drops them with it. Returns a new `DropColumnResult { description, dropped_indexes }`; the runtime reports each dropped index. - **Persistence:** an additive `indexes:` list in `project.yaml` (`version:` unchanged — older files default it empty). `read_schema_snapshot` writes it, `do_rebuild_from_text` recreates indexes on rebuild; `export` / `import` carry them. - **Display:** an `Indexes:` section in `render_structure`; the nested items panel via a new `SchemaCache::table_indexes` map. - **Tests:** 30 new across tiers — 6 parser, 13 db integration (real in-memory SQLite), 2 `output_render`, 1 full project rebuild round-trip (`tests/iteration3_rebuild.rs`), 8 typing-surface matrix cells (`tests/typing_surface/index_ops.rs`). ### Input-history cursor fix (`4e4dbbf`) See §2. ## §2. Bug found and fixed this session **Input-history cursor stranded on re-submit.** `App::push_history` did its `history_cursor` / `history_draft` reset *after* the consecutive-duplicate early-return. So recalling a command with Up and re-submitting it unchanged (a duplicate → not appended → reset skipped) left the cursor pinned at that entry; the next Up then stepped *backwards* from there (`Some(i) => i-1`) instead of restarting at the newest entry — the next Up appeared to "skip" an entry. Fixed by moving the reset ahead of the early-return guards — submitting a command always ends history navigation. Tier-1 regression test `app::tests::resubmitting_a_recalled_command_does_not_strand_the_cursor` drives the recall/resubmit keystroke sequence. ## §3. Notes for the next agent **Two ADR-0025 deviations are recorded** in that ADR's `## Implementation notes` section — read them, the decision text and the code agree there but the sketch above it does not: - `rename column` / `drop column` use native `ALTER TABLE`, not the rebuild primitive. SQLite's `RENAME COLUMN` already rewrites index definitions, so rename needs no index code; `drop column` handles covering indexes directly. Only `change column` (and the relationship ops) go through the rebuild primitive, where the column set is unchanged, so indexes are recreated verbatim — no rename-map needed. - The S2 panel carries per-table index names in `SchemaCache::table_indexes` rather than restructuring `app.tables` and the `TablesRefreshed` event. **One defensively-unreachable branch.** `do_drop_index`'s "more than one index matches these columns" arm cannot be reached via the app's own commands — `do_add_index` refuses a redundant index over an identical column set. It is reachable only via a hand-edited `project.yaml`, which the project does defend against elsewhere. Left in, untested. ## §4. What's next — QA1 is unblocked `docs/requirements.md` was reconciled this session: the index portion of `C3` is done, **`S2` moved to `[x]`**, the `QA1` note updated, test baseline refreshed to 1039. **The natural next pick is `QA1` — `EXPLAIN QUERY PLAN`.** Until this session there was no index for a plan to be interesting about; now `show data where = ` (the C5 filtered form, already implemented) is a query whose plan visibly flips between a full scan and an index search. But QA1 has an unmet dependency of its own: **`QA2`** (plan rendering — tree layout, annotation taxonomy, colour scheme) is `[~]` and **needs an ADR before any implementation**. So QA1 is "write the QA2 ADR, then build" — not a self-contained drop-in. **Prioritisation is a user product decision — ask, don't assume.** Other open clusters in `requirements.md` (unchanged from handoff-14 §4, minus indexes): - **Complex WHERE expressions** (`C5a`, `[~]`) — AND/OR / comparison / LIKE in UPDATE/DELETE/show-data filters. Needs an ADR. The bridge from DSL toward real SQL. - **SQL in advanced mode** (`Q1`/`Q4`, `[~]`) — `sqlparser-rs` + a defined subset. Needs an ADR. - **Snapshot / undo** (`U1`/`U2`) — designed in ADR-0006, not built. - **m:n convenience** (`C4`), **modify relationship** (`C3a`, `[~]`), **table rename** (`C1`). - **Friendly error layer** (`H1`) — partial. **Syntax-help in parse errors** (`H1a`) — piecemeal. - **Session log + Markdown export** (`V4`, `[~]`), **multi-line input** (`I1`), **readline shortcuts** (`I1b`), **seeding** (`SD1`), **CI** (`TT5`), **tutorial system** (`TU1`, `[~]`). ## §5. Architectural delta (vs. handoff-14) ### New `Command` variants / fields - `Command::AddIndex { name: Option, table, columns }`. - `Command::DropIndex { selector: IndexSelector }`. - `IndexSelector` enum — `Named` / `Columns`. - `Command::DropColumn` gained `cascade: bool`. ### New grammar surface - `IdentSource::Indexes` — completion source for existing index names. - `add` / `drop` `Choice`s gained index branches; `drop column` gained the `--cascade` flag node. ### New db surface - `IndexInfo` (public) — on `TableDescription::indexes`. - `DropColumnResult { description, dropped_indexes }` — the new return type of `Database::drop_column` / `Request::DropColumn` / `do_drop_column`. - `Request::AddIndex` / `DropIndex`; `Database::add_index` / `drop_index`. - `read_table_indexes` / `read_index_columns` helpers; `do_add_index` / `do_drop_index`. - `rebuild_table_with_copy` is now index-preserving. ### New event / outcome / friendly surface - `AppEvent::DslDropColumnSucceeded { command, result }`; `App::handle_dsl_drop_column_success`. - `CommandOutcome::DropColumn(DropColumnResult)`. - `friendly::Operation::AddIndex` / `DropIndex`. ### `SchemaCache` - `indexes: Vec` — flat index-name list (for `drop index ` completion). - `table_indexes: HashMap>` — per-table index names (for the S2 panel). ### Persistence - `IndexSchema { name, table, columns }`; `SchemaSnapshot.indexes`; a `indexes:` block in the `project.yaml` writer + an optional `#[serde(default)]` field in the reader. ### Catalog - Added: `parse.usage.add_index`, `parse.usage.drop_index`, `ok.index_dropped_with_column`. Modified: `help.ddl.add`, `help.ddl.drop`. New keys registered in `src/friendly/keys.rs`. ### Tests - New matrix file `tests/typing_surface/index_ops.rs` (8 cells) → 152 matrix cells total. ## §6. How to take over 1. **Read this file, then handoff-14, then 13** for the chain. 2. **Read `CLAUDE.md`** — the working-style rules. This session escalated all four index design forks to the user before drafting the ADR. 3. **Read `docs/requirements.md`** — reconciled this session; the authoritative "what's open" tracker. 4. **Read `docs/adr/0025-indexes.md`** — including its `## Implementation notes` (§3 above). 5. **Run `cargo test`** — 1039 passing, 0 failing, 1 ignored. 6. **Run `cargo clippy --all-targets -- -D warnings`** — clean. 7. **`4e4dbbf` is unpushed** — the user pushes; don't treat it as blocking. 8. **Pick the next work from §4** — `QA1` is the natural pick but it needs the `QA2` rendering ADR first, and prioritisation is a user product decision. Ask, don't assume. ### Note on the typing-surface matrix `tests/typing_surface/` (now 152 cells) is the regression net for everything walker/hint/completion. After any grammar or walker change: a failing cell with *correct* new behaviour → update its snapshot (`INSTA_UPDATE=always cargo test --test typing_surface_matrix `); a failing cell with *wrong* behaviour → the cell earned its keep. Adding `add index` shifted one `add_relationship` cell's snapshot this session (the `add ` candidate set gained `index`) — that was a correct update.