diff --git a/docs/handoff/20260525-handoff-39.md b/docs/handoff/20260525-handoff-39.md new file mode 100644 index 0000000..2c9f20d --- /dev/null +++ b/docs/handoff/20260525-handoff-39.md @@ -0,0 +1,214 @@ +# Session handoff — 2026-05-25 (39) + +Thirty-ninth handover. This session **shipped ADR-0035 Phase-4 sub-phases +4d and 4e** (each test-first, with a `/runda` planning gate *and* a +`/runda` finished-slice critique, all green) and then **fully prepared +4f** — the plan is written, `/runda`'d, and forks resolved, ready for a +fresh session to implement. The next session **implements 4f — +`ALTER TABLE … ALTER COLUMN TYPE`** from +`docs/plans/20260525-adr-0035-sql-ddl-4f.md` (no open decisions). + +**⚠️ Read §1.1 first — there is a git divergence to reconcile.** + +## §1. State at handoff + +**Branch:** `main`. **Tests: 1854 passing, 0 failing, 0 skipped, +1 ignored** (the unchanged `friendly/mod.rs` ` ```ignore ` doctest). +**Clippy:** clean (`cargo clippy --all-targets -- -D warnings`). + +**This session's commits** (oldest → newest, local): + +``` +701217d feat: ADR-0035 4d — CREATE [UNIQUE] INDEX / DROP INDEX (amended) +bbc2e34 feat: ADR-0035 4e — ALTER TABLE add/drop/rename column +``` + +Uncommitted in the working tree: `docs/plans/…-4f.md`, +`docs/handoff/…-39.md` (this file) — to be committed with user approval. + +### §1.1 ⚠️ Git divergence (origin/main vs local 4d) — user action needed + +`origin/main` is at **`3247e3c`** — the **pre-amend** 4d commit. The 4d +work was first committed as `3247e3c` and **pushed**; then, during the 4d +finished-slice `/runda`, the user chose "amend `3247e3c`" to fold in one +extra test (the unnamed `CREATE INDEX IF NOT EXISTS` skip) + the +internal-`__rdbms_*` guard, producing **`701217d`**. So the pushed 4d +(`3247e3c`) and the local 4d (`701217d`) are **sibling commits** (same +parent, different content) — the histories diverged at 4d. + +Consequence: a plain `git push` will be rejected (non-fast-forward). The +delta between `3247e3c` and `701217d` is exactly the two `/runda` +additions (one test + the `do_add_index` internal-table guard + their +doc lines). Reconciliation is the **user's call** (this is a single-dev +repo, so a force-push to replace `3247e3c` with `701217d`+`bbc2e34` is +the likely intent) — **agents must not push or force-push.** Options for +the user: +- `git push --force-with-lease` to replace the pushed 4d with the amended + history (single-dev branch → low risk); or +- cherry-pick only the amend delta as a *new* commit on top of `3247e3c` + if a clean linear forward-only history is preferred. + +Do **not** re-amend or rebase further without deciding this first. + +## §2. What shipped this session + +- **4d — `CREATE [UNIQUE] INDEX` / `DROP INDEX [IF EXISTS]`** (`701217d`). + Reuses `do_add_index` / `do_drop_index`. **`CREATE UNIQUE INDEX` + admitted in advanced mode** (ADR-0025 **Amendment 1**) via an additive + `IndexSchema.unique` flag (round-trips `project.yaml`, survives + rebuild, `[unique]` markers in the structure view + items panel); + simple `add unique index` stays deferred. `IF [NOT] EXISTS` reuses the + 4c skip path. `create`/`drop` each gained a *second* advanced node + (all-candidates dispatch). `do_add_index` gained an internal-`__rdbms_*` + guard (both surfaces). +- **4e — `ALTER TABLE` add/drop/rename column** (`bbc2e34`). `alter` is a + new advanced-**only** entry word; `SqlAlterTable { AlterTableAction }` + is **runtime-decomposed** to the existing `do_add_column` / + `do_drop_column` / `do_rename_column` (one undo step each — no new + worker layer). `do_add_column` **extended to consume raw + `default_sql`/`check_sql`** so ADD COLUMN reaches CREATE-TABLE + constraint parity. Drop/rename **refuse a column any CHECK references** + — table-level *and* column-level (incl. a column's own self-check on + rename), via the `check_references_column` tokenizer in the shared + executors (both surfaces) — fixing a latent rename-drift bug. SQL DROP + COLUMN refuses an index-covered column (no `--cascade` SQL spelling). + The column executors + `do_add_index` carry the internal-`__rdbms_*` + guard. + +ADR-0035 stays **Accepted**; README + `requirements.md` Q1 + ADR §13 +updated each slice; ADR-0025 Amendment 1 for the unique index. + +## §3. The NEXT job — 4f (`ALTER TABLE … ALTER COLUMN TYPE`) + +**Plan: `docs/plans/20260525-adr-0035-sql-ddl-4f.md` — read it; it is +`/runda`'d and all forks are resolved.** Headlines: + +- A fourth `AlterTableAction::AlterColumnType { column, ty }`; + `ALTER TABLE ALTER COLUMN TYPE ` → a fourth action + `Choice` branch (`AT_ALTER_COLUMN`, leads `alter`), builder + discriminated by the **`type` keyword** (`contains_word("type")`, + checked first — unique to this action; ADD COLUMN's type is an *ident*). + Reuse `super::sql_create_table::SQL_TYPE` for the type slot. +- **Runtime-decompose** to `database.change_column_type(table, column, ty, + ChangeColumnMode::ForceConversion, src)` → `CommandOutcome::ChangeColumn` + (the existing simple-`change column` path — surfaces the client-side + lossy note). **No new mode, no new note, no new persistence.** +- **`ForceConversion` IS the §7 advanced policy** (verified in code): + incompatible cells refuse; lossy cells are *performed* (the dry-run + skips the refuse only for `ForceConversion`, db.rs:4575) and counted → + the existing engine-neutral `client_side.transformed_lossy` note fires. +- **Static refusals (verified `type_change::static_refusal`):** + same-type; **non-int → serial** (only `int → serial` allowed); any + `↔ blob`; direct `date ↔ datetime`; not-in-matrix. **⚠️ `int → serial` + is ALLOWED** (auto-fill nulls + UNIQUE, ADR-0018 §8) — ADR-0035 §7's + "static-refused →serial" summary is looser than the code. Test + `int → serial` as *supported*, `text → serial` / `↔ blob` as refused. + **Do not "fix" int→serial to refuse.** +- **Fold the internal-`__rdbms_*` guard into `do_change_column_type`** + (user-confirmed 2026-05-25, both surfaces) — closes the simple `change + column` exposure. (`do_add_constraint` / `do_add_relationship` stay a + **4g** follow-up.) +- Out: `SET DATA TYPE` synonym, Postgres `USING`, any SQL spelling of + `--force-conversion` / `--dont-convert` (ADR §7/§12). + +Implementation sequence + the full checklist are in the plan §9/§3. +Test-first; mirror the 4e slices (`tests/sql_alter_table.rs` e2e via +`run_replay`; `tests/column_op_guards.rs` for the simple-surface guard; +the `sql_alter_table_tests` in `ddl.rs` for parse). + +## §4. Everything else remaining in Phase 4 (ADR-0035 §13) + +In order after 4f: +- **4g — `ALTER TABLE` add/drop constraint, add FK.** The add-FK path + reuses the 4b shared relationship helpers; named CHECK adds a `name` + column to `__rdbms_playground_table_checks`. **Fold the internal-table + guard into `do_add_constraint` / `do_add_relationship` here** (the + remaining executors of the 4d/4e/4f guard class). +- **4h — `ALTER TABLE … RENAME TO`** (the §6 new low-level op: rename + table + `data/.csv` + relationship metadata + `table_checks` rows). +- **4i — Verification sweep** (the running deferral tail — §5 below). + +## §5. The 4i deferral tail (canonical: ADR-0035 §13 4i) + +Unchanged from handoff-38 except as noted: (a) CREATE TABLE help/usage +skeleton refresh — **4d/4e/4f index/ALTER forms carry their own +help/usage** (only the *CREATE TABLE* skeleton refresh remains); (b) +`describe` of table-level constraints (composite `UNIQUE` + table +`CHECK`) — the **unique-index marker shipped in 4d**, so only table-level +*constraint* display remains; (c) 4b self-ref FK pre-submit indicator; +(d) shared-entry-word completion merge — **widened by 4d** (`create`/ +`drop` now have two advanced nodes each); (e) the simple/advanced +completion-colour UX discussion (user flag). + +## §6. Tracked follow-ups (not lost) + +- **Internal-`__rdbms_*` guard on the remaining executors** — + `do_change_column_type` is closed in **4f**; `do_add_constraint` / + `do_add_relationship` ride in **4g**. (The pattern: a + `reject_internal_table_name(table)?` at the top of each user-facing + schema-mutation executor; `do_add_index` + the three column executors + already have it.) +- **H1 friendly wording** for the CHECK-guard refusals (4e) and the + type-conversion diagnostics — the current messages are engine-neutral + but terse. +- The §8 items carried from handoff-38 (app-lifecycle-command + runtime-failure journalling (A); M4 execution-time mode side-channel; + `blob` value literal; CI/TT5; DSL→SQL teaching echo) remain open. + +## §7. Patterns the implementer must not forget + +1. **Reuse low-level executors.** `Sql*` DDL wraps the existing helpers + (4f wraps `change_column_type` with `ForceConversion`); they do not + fork. The runtime decomposes `SqlAlterTable` per action. +2. **Two DDL generators stay in sync** (`do_create_table` / + `schema_to_ddl`) — not directly touched by 4f, but the rebuild path + is the round-trip net for the type change. +3. **Grammar leading-`Optional` trap** — each `Choice` branch leads on a + concrete keyword (4f's `AT_ALTER_COLUMN` leads `alter`). +4. **Exhaustive matches:** a new `AlterTableAction` variant forces arms + in `runtime.rs` (the inner `match action`) and `app.rs` + (`build_translate_context`'s inner match). The `Command`-level + matches (verb/target_table/typing-surface) are unchanged (the variant + is internal to `SqlAlterTable`). The compiler finds them. +5. **Catalog lockstep + vocab audit** — 4f *refreshes* the existing + `sql_alter_table` help/usage (no new keys); keep wording + engine-neutral. +6. **Probe, don't reason** — verify the `type`-keyword discriminator and + the `SQL_TYPE` extraction (mirror `build_alter_add_column_spec`) with + a parse test before wiring execution. + +## §8. Process pins (unchanged, still binding) + +- **Confirm every commit.** Propose the message; wait for the go-ahead. +- **Push is the user's step.** Never push; never force-push. **See §1.1.** +- **No AI attribution** in commits. +- **Escalate ambiguity / new cost.** 4f's one fork (the + `do_change_column_type` internal guard) was surfaced + user-confirmed + 2026-05-25; the §7 lossy policy + the no-force/no-`USING` exclusions + are ADR-settled. +- **DA hat each slice; `/runda` at the planning exit AND on the finished + slice.** Both 4d and 4e had a planning `/runda` and a finished-slice + `/runda`; each finished-slice pass found a real gap (4d: unnamed-skip + coverage; 4e: column-level CHECK rename-drift) that was fixed before + commit. Budget for the same on 4f. +- **Keep docs lockstep** — ADR status/§13 + README + `requirements.md` in + the same edit. + +## §9. How to take over + +1. **Resolve §1.1** (the git divergence) with the user before any new + commit work. +2. **Read, in order:** this file → `docs/plans/20260525-adr-0035-sql-ddl-4f.md` + → `docs/adr/0035-advanced-mode-sql-ddl.md` (§4 ALTER row, §7 + conversion, §13 4f) → `docs/adr/0017-column-type-change-compatibility.md` + (the matrix + static refusals) → `CLAUDE.md` → `docs/requirements.md` + (`Q1`). +3. **Baseline:** + ``` + cargo test # expect 1854 passing / 0 failing / 0 skipped / 1 ignored + cargo clippy --all-targets -- -D warnings # clean + ``` +4. **Implement 4f** per the plan §9 (test-first): internal guard first, + then command/grammar/builder (probe the `type` discriminator), then + runtime + catalog, then the full sweep + docs + `/runda` + commit + proposal. diff --git a/docs/plans/20260525-adr-0035-sql-ddl-4f.md b/docs/plans/20260525-adr-0035-sql-ddl-4f.md new file mode 100644 index 0000000..bf22a21 --- /dev/null +++ b/docs/plans/20260525-adr-0035-sql-ddl-4f.md @@ -0,0 +1,256 @@ +# Plan: ADR-0035 Phase 4, sub-phase 4f — `ALTER TABLE … ALTER COLUMN TYPE` + +Add `ALTER TABLE ALTER COLUMN TYPE ` as a fourth +`AlterTableAction` (`AlterColumnType`), runtime-decomposed to the +existing `do_change_column_type` executor with the **advanced-mode +policy** (ADR-0035 §7): lossy conversions are **performed with a note**, +no force flag; static-refused (`→serial`, `↔blob`) and incompatible +conversions still refuse (ADR-0017, shared by both modes). This is the +smallest 4e-style slice — almost entirely reuse. + +The single new executor change is folding the internal-`__rdbms_*` guard +into `do_change_column_type` (the 4e follow-up), so the **simple `change +column` surface** is closed too. + +## 1. Baseline (at handoff) + +- Tests: **1854 passing, 0 failing, 0 skipped, 1 ignored**; clippy clean. + Branch `main`, HEAD `bbc2e34` (4e). 4f starts here. (4a–4e are + local-only commits since `origin/main`.) + +## 2. Decisions (settled — ADR §7/§13 4f + user-confirmed 2026-05-25) + +1. **Advanced lossy = `ChangeColumnMode::ForceConversion`** — no new + mode, no new note. `ForceConversion` already *is* the §7 advanced + policy: `static_refusal` refuses `→serial`/`↔blob`; the dry-run + refuses incompatible cells; lossy cells are transformed; the + `ClientSideNote { transformed, lossy }` is surfaced. The existing + `client_side.transformed_lossy` catalog note (*"N row(s) transformed; + M of those discarded information (lossy)…"*) is engine-neutral and + reads correctly for advanced — it is the §7 "N values converted with + loss" note. (Update the stale `// fires under --force-conversion` + comment in `en-US.yaml` to mention advanced ALTER COLUMN TYPE too.) +2. **No SQL spelling for `--force-conversion` / `--dont-convert`** + (ADR §7/§12). Advanced always performs the conversion (relying on + `undo` as the safety net — a payoff of shipping ADR-0006 first). The + Postgres `USING ` clause is **not** adopted (§12). +3. **Static-refused + incompatible reuse the executor** (ADR-0017, + shared by both modes). **Verified `static_refusal` (type_change.rs) + refuses:** same-type (`already X`); **non-int → `serial`** (only + `int → serial` is allowed); any `↔ blob`; direct `date ↔ datetime` + (route via text); and any pair not in the matrix. An *incompatible* + per-cell conversion → the existing diagnostic. **NOTE — ADR-0035 §7's + "static-refused (→serial …)" summary is looser than the code: + `int → serial` IS allowed** (ADR-0018 §8 — auto-fills null cells with + generated values, adds UNIQUE if non-PK), so advanced + `ALTER COLUMN c TYPE serial` on an `int` column is a *supported* + operation, not a refusal. The implementer must test `int → serial` + (allowed) and `text → serial` / `↔ blob` (refused) — do NOT "fix" + int→serial to refuse. +4. **`TYPE` keyword only** — `ALTER COLUMN TYPE ` (ADR §4). + The Postgres `SET DATA TYPE` synonym is **not** added (kept minimal; + could be a later alias). +5. **Internal-`__rdbms_*` guard folded into `do_change_column_type`** + (user-confirmed 2026-05-25) — closes the simple `change column` + exposure alongside the parse-time guard the SQL ALTER table slot + already gives. `do_add_constraint` / `do_add_relationship` stay + flagged for **4g**. + +## 3. Phase 1 — Requirements checklist (4f) + +Grammar / dispatch: +- [ ] `ALTER TABLE ALTER COLUMN TYPE ` parses in + advanced mode → `SqlAlterTable { AlterColumnType { column, ty } }`; + `` accepts the SQL type vocabulary + aliases (reuse `SQL_TYPE`). +- [ ] The action `Choice` now has **four** branches (add/drop/rename/ + alter-column), each leading on a distinct concrete keyword — trap-safe. + The other three still parse; `alter` remains advanced-only; the table + slot still rejects `__rdbms_*` at parse. +- [ ] Trailing `;` tolerated. + +Execution (reuse `do_change_column_type` with `ForceConversion`): +- [ ] **Clean** conversion (e.g. `int → text`) auto-converts; one undo + step; auto-show. +- [ ] **Lossy** conversion (e.g. `real → int`, `3.7 → 3`) is **performed** + and surfaces the `[client-side] … discarded information (lossy)` note + (no force flag, no refusal) — the §7 advanced behaviour. +- [ ] **Incompatible** conversion is refused (friendly, engine-neutral) + — e.g. `text → int` where a cell holds a non-numeric string. +- [ ] **Static-refused** refused: `text → serial` (non-int source), + `↔ blob`, `date ↔ datetime` direct, same-type. **And** `int → serial` + is **allowed** (auto-fills nulls, adds UNIQUE — ADR-0018 §8): test it + as a supported conversion, not a refusal. +- [ ] FK-involved column type change refuses per the existing executor + rules (outbound/inbound), via the SQL path too. +- [ ] The converted column round-trips and **survives rebuild** (the + `user_type` metadata update is the existing path). + +Internal-table guard (both surfaces): +- [ ] `do_change_column_type` refuses an internal `__rdbms_*` table (as + "no such table"); the simple `change column` surface is closed. A test + on the simple surface. + +### Testing +- [ ] **Tier 1** (extend `sql_alter_table_tests` in `ddl.rs`): the + alter-column-type parse (incl. a type alias) → `AlterColumnType`; the + four-branch dispatch still routes add/drop/rename correctly. +- [ ] **Tier 3** (extend `tests/sql_alter_table.rs`, via `run_replay`): + clean convert (`int → text`); lossy convert (`real → int`) performs — + assert the value was converted (e.g. `3.7` stored as `3`); incompatible + refused; `text → serial` (and/or `↔ blob`) refused; **`int → serial` + allowed** (auto-fill); rebuild survival; one undo step. (Assert the + lossy *data* conversion as the primary signal; the `[client-side] … + lossy` note is rendered through the shared ChangeColumn path.) +- [ ] **Internal guard** (extend `tests/column_op_guards.rs`): simple + `change column` on `__rdbms_*` refused. +- [ ] **Catalog** lockstep + vocab audit for the refreshed + `sql_alter_table` help/usage (it now lists the alter-column-type form). + +## 4. Architecture & design + +### 4.1 Command (`src/dsl/command.rs`) +- `AlterTableAction` gains `AlterColumnType { column: String, ty: Type }` + (small variant — no boxing needed; the boxed `AddColumn` already + dominates the enum's size). + +### 4.2 Grammar (`src/dsl/grammar/ddl.rs`) +- New action branch `AT_ALTER_COLUMN = Seq[ Word("alter"), + Word("column"), COLUMN_NAME, Word("type"), + super::sql_create_table::SQL_TYPE ]`. Add it to `AT_ACTION_CHOICES` + (now `[AT_ADD_COLUMN, AT_DROP_COLUMN, AT_RENAME_COLUMN, + AT_ALTER_COLUMN]`). Leads on the concrete `alter` keyword (distinct + from add/drop/rename) — trap-safe. The action's `alter` is the 4th + token (the entry-word `alter` is consumed by dispatch first). +- `build_sql_alter_table`: add a branch. Discriminate on the **`type` + keyword** (`path.contains_word("type")`) — unique to ALTER COLUMN TYPE + (ADD COLUMN's type is a `col_type` *ident*, not the literal `type` + keyword). Extract `column = require_ident("column_name")` and the + target `Type` from the `col_type` ident (`Type::from_sql_name`) / + the `double precision` two-word case — mirror the type-extraction in + `build_alter_add_column_spec`. **Order the `type` check before + add/rename/drop.** + +### 4.3 Worker / executor (`src/db.rs`) +- **No new method.** Add the internal-table guard: + `reject_internal_table_name(table)?;` at the top of + `do_change_column_type` (mirrors the 4e column executors). +- `ForceConversion`'s lossy path + `ClientSideNote` are unchanged. + +### 4.4 Runtime (`src/runtime.rs`) +- Extend the `SqlAlterTable` action match: + `AlterColumnType { column, ty } => database.change_column_type(table, + column, ty, ChangeColumnMode::ForceConversion, src).await + .map(CommandOutcome::ChangeColumn)` — the same outcome the simple + `change column` uses (its event/app rendering surfaces the client-side + lossy note). + +### 4.5 app.rs failure-translate + typing_surface +- `app.rs build_translate_context`: the `SqlAlterTable` arm's inner + `match action` gains `AlterColumnType { column, .. } => + (Operation::ChangeColumnType, Some(table), Some(column))`. +- `typing_surface` already labels `SqlAlterTable`; no change (the action + is internal to the variant). + +### 4.6 Catalog (`keys.rs` + `en-US.yaml`) +- **Refresh** the existing `help.ddl.sql_alter_table` + + `parse.usage.sql_alter_table` bodies to add the + `alter table alter column type ` line. No new keys. +- Update the stale `client_side` `// fires under --force-conversion` + comment to note advanced ALTER COLUMN TYPE also triggers the lossy + note (comment only). + +## 5. Phase 2 — Candidate approaches + +**(M1) Reuse `do_change_column_type` with `ForceConversion`** *(lead)* — +the mode already encodes the §7 advanced policy exactly; the lossy note +already exists and is engine-neutral. Zero executor logic beyond the +internal-table guard. +**(M2) Add a new `ChangeColumnMode::Advanced` variant** — *rejected.* +It would duplicate `ForceConversion`'s behaviour; the only conceivable +difference (note wording) is already neutral and correct. No new variant +earns its keep. (If a future need to distinguish "user forced" from +"advanced auto" arises — e.g. for telemetry — revisit then.) + +**(G1) A fourth action branch leading on `alter`, discriminated in the +builder by the `type` keyword** *(lead)* — consistent with the three +4e branches; `type` is an unambiguous discriminator. +**(G2) A separate `SQL_ALTER_COLUMN` command/node** — *rejected.* The +`AlterTableAction` enum is the established extension point (built for +this in 4e); a separate command would fork the `alter table` dispatch. + +## 6. Phase 3 — Selection vs the checklist + +M1 + G1 satisfy every §3 item: parse (G1 four-branch), the §7 tiers +(clean/lossy/incompatible/static-refused — all from the reused executor ++ `ForceConversion`), the lossy note (existing `transformed_lossy`), +rebuild survival + undo parity (the executor's existing rebuild path), +the internal-table guard (one-line add, both surfaces). No requirement +unmet, no new model/persistence. **Selected: M1 + G1.** + +## 7. Devil's Advocate review of this plan + +- **Fork escalated?** The only 4f fork — the internal-table guard on + `do_change_column_type` — was put to the user (2026-05-25, "fix in 4f, + both surfaces"). The §7 lossy policy and the no-force-flag/`USING` + exclusions are ADR-settled, not autonomous. ✓ +- **Is `ForceConversion` truly the §7 advanced policy? — verified in + code.** `run_change_column_with_dry_run`: incompatible cells refuse in + both modes; lossy cells refuse **only when `mode != ForceConversion`** + (db.rs:4575) — so under `ForceConversion` they are performed and + counted (`ClientSideNote.lossy`), firing `client_side.transformed_lossy` + (engine-neutral, no `--force-conversion` in the rendered text). + `static_refusal` was read in full (the §2.3 list) — **correcting the + plan's first draft**: `int → serial` is *allowed*, not refused. Matches + the §7 intent once that nuance is captured. ✓ +- **Grammar trap / discriminator?** Four concrete-keyword-led branches; + the builder keys on the `type` keyword (unique — ADD COLUMN's type is + an ident). A parse test for all four branches + the alias case is on + the checklist. The implementer must **probe** the `type`-keyword + discriminator and the SQL_TYPE extraction (mirror + `build_alter_add_column_spec`) — low-risk, 4e-proven patterns. ✓ +- **Lossy note actually fires on the SQL path?** The e2e (`run_replay`) + lossy test asserts it; the note path is `CommandOutcome::ChangeColumn` + → the existing app rendering, shared with simple `change column`. ✓ +- **Undo parity?** One `change_column_type` call = one snapshot (the + executor's existing rebuild is one undo step). A per-action undo check + in the e2e. ✓ +- **Anything dropped?** `SET DATA TYPE` synonym and the `--force/--dont` + SQL spellings are *deliberately* out (stated, ADR-backed). The broader + internal-table guard (`do_add_constraint` / `do_add_relationship`) + stays a **4g** follow-up — flagged, not silent. ✓ +- **Help/usage refresh?** The `sql_alter_table` skeleton is *refreshed* + (existing node, existing keys) to list the new form — not deferred to + 4i (that deferral is only for the *CREATE TABLE* skeleton). ✓ + +## 8. Out of 4f scope (tracked, not dropped) +- `ALTER TABLE` add/drop constraint + add FK (**4g**) — and the internal + guard on `do_add_constraint` / `do_add_relationship` rides there. +- `ALTER TABLE … RENAME TO` table rename (**4h**). +- `SET DATA TYPE` synonym; the Postgres `USING ` clause; any SQL + spelling of `--force-conversion` / `--dont-convert` (ADR §7/§12). +- 4i verification sweep (the running deferral tail). + +## 9. Implementation sequence (test-first) +1. **Internal-table guard (isolated, both surfaces).** + `reject_internal_table_name` at the top of `do_change_column_type`; + a Tier-3 test via the simple `change column` (`column_op_guards.rs`) + → green. Lands the latent-bug fix on its own. +2. **Command + grammar + builder.** `AlterColumnType` variant; the + `AT_ALTER_COLUMN` branch + the builder discriminator; Tier-1 parse + + four-branch dispatch tests (probe the `type` discriminator first) → + the exhaustive arms (app.rs) → green (parse only). +3. **Runtime + catalog.** Wire `AlterColumnType` → + `change_column_type(…, ForceConversion)`; refresh the + `sql_alter_table` help/usage; Tier-3 (`sql_alter_table.rs` via + `run_replay`): clean / lossy+note / incompatible / static-refused / + rebuild / undo → green. +4. **Full sweep** — `cargo test` (no regression from 1854) + `cargo + clippy --all-targets -- -D warnings`. +5. **Docs** — ADR-0035 Status + §13 4f implemented; README; requirements + Q1. Run `/runda` over the finished slice. Propose commit; wait for + approval. + +## 10. Exit gate +- All §3 items satisfied; four tiers green, zero skips; no regression + from 1854; `/runda` / written-DA PASS; clippy clean; ADR-0035 §13 4f + + README + requirements.md lockstep.