docs: 4f plan + session handoff 39
- docs/plans/20260525-adr-0035-sql-ddl-4f.md — ALTER TABLE ALTER COLUMN TYPE plan (/runda'd; forks resolved). Advanced lossy = ForceConversion (reuse do_change_column_type; existing transformed_lossy note); the internal-__rdbms_* guard folds into do_change_column_type both surfaces (user-confirmed); int->serial is ALLOWED (ADR-0018 §8), only non-int ->serial / blob / date<->datetime are static-refused. - docs/handoff/20260525-handoff-39.md — 4d/4e shipped; 4f is next (plan ready).
This commit is contained in:
@@ -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 <T> ALTER COLUMN <col> TYPE <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/<t>.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.
|
||||||
@@ -0,0 +1,256 @@
|
|||||||
|
# Plan: ADR-0035 Phase 4, sub-phase 4f — `ALTER TABLE … ALTER COLUMN TYPE`
|
||||||
|
|
||||||
|
Add `ALTER TABLE <T> ALTER COLUMN <col> TYPE <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 <expr>` 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 <col> TYPE <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 <T> ALTER COLUMN <col> TYPE <type>` parses in
|
||||||
|
advanced mode → `SqlAlterTable { AlterColumnType { column, ty } }`;
|
||||||
|
`<type>` 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 <T> alter column <col> type <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 <expr>` 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.
|
||||||
Reference in New Issue
Block a user