# 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 <name>] on <T>
  (<cols>)`; auto-name `<Table>_<col…>_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 <name>` /
  `drop index on <T> (<cols>)`). `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 <T> where <col> = <val>`
(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<String>, 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<String>` — flat index-name list (for
  `drop index <name>` completion).
- `table_indexes: HashMap<String, Vec<String>>` — 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
<family>`); 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.
