# Phase 3 — Generate SQL DDL (tables, indexes, FKs, constraints)

Emit a full `db/schema.sql` file based on `DB`, `ORM`, `STYLE`, and
`ENTITIES`. Then ONE `AskUserQuestion` to review/revise.

## 3a — Pick primary-key strategy (inline, no AskUserQuestion — deterministic)

- Postgres 17: `id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY`
  (SQL-standard, PG 10+; avoid legacy `SERIAL`). See `db-postgres.md`.
- SQLite: `id INTEGER PRIMARY KEY` (rowid alias; autoincrement discouraged
  unless monotonic guarantee required). See `db-sqlite.md`.
- MySQL: `id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY`.

If the user's fields include `id` already, respect it; otherwise prepend
the surrogate PK. Record the choice in state for Phase 4 traceability.

## 3b — Per-entity DDL generation rules

For each entity in `ENTITIES`:

1. **CREATE TABLE**: `CREATE TABLE IF NOT EXISTS <snake_case_name>` (plural
   unless user named it singular — default to pluralize: `User` → `users`,
   `Organization` → `organizations`; junction `OrganizationUser` →
   `organization_users`).
2. **Columns**: infer SQL type from field name + heuristics:
   - `*_id`, `id` → `BIGINT` (Postgres/MySQL) / `INTEGER` (SQLite).
   - `*_at`, `created_at`, `updated_at` → `TIMESTAMPTZ` (PG) / `DATETIME`
     (SQLite) / `TIMESTAMP` (MySQL), `DEFAULT now()` (PG) /
     `DEFAULT CURRENT_TIMESTAMP` (others).
   - `email`, `*_email` → `TEXT` / `VARCHAR(320)` (RFC 5321 limit) + `NOT
     NULL` + `CHECK (email LIKE '%@%')` (cheap sanity, real validation is
     app-side).
   - `*_count`, `*_amount` → `INTEGER` or `NUMERIC(18,2)` (money);
     default `0`.
   - `is_*`, `has_*` → `BOOLEAN NOT NULL DEFAULT false`.
   - Freeform → `TEXT NOT NULL` unless user said "optional".
3. **Timestamps default**: unless user opts out, add `created_at` +
   `updated_at` to every non-junction entity.
4. **Soft-delete**: if `SCALE = Production multi-replica`, add
   `deleted_at TIMESTAMPTZ NULL` + partial index (PG only) on
   `WHERE deleted_at IS NULL`.

## 3c — Foreign keys

For each `relations` entry (kind ≠ `None`):

- `REFERENCES <parent_table>(id) ON DELETE <ACTION>`.
- Default `ON DELETE` action: junction → `CASCADE`; one-to-one → `CASCADE`
  from owning side; one-to-many → `RESTRICT` (safer default; explicit
  cascade is a product decision). See `db-migration-hygiene.md`.
- **Mandatory FK index**: every FK column gets `CREATE INDEX
  IF NOT EXISTS idx_<table>_<col> ON <table>(<col>);`. Unindexed FKs =
  join explosion (see `db-postgres.md` "Forbidden").

## 3d — Indexes + constraints

- Unique: `email`, `slug`, `username`, any field user marked `UNIQUE` →
  `UNIQUE` column constraint.
- Composite indexes: for junction tables, PK is composite; add a
  reverse-order index if both directions of lookup are common.
- Check constraints: `CHECK (status IN (...))` for enum-ish text fields;
  `CHECK (amount >= 0)` for non-negative numerics.
- Triggers: if `updated_at` present AND `DB = Postgres`, emit a
  `CREATE OR REPLACE FUNCTION set_updated_at()` + `CREATE TRIGGER` per
  table. SQLite uses `AFTER UPDATE` triggers; MySQL uses `ON UPDATE
  CURRENT_TIMESTAMP` in the column definition.

## 3e — Emit the file

Write `db/schema.sql` with a top-level comment:

```sql
-- Generated by /schema-design — <YYYY-MM-DD>
-- DB: <DB>  ORM: <ORM>  Style: <STYLE>
-- Entities: <N>  Relations: <M>
-- Edit freely; Phase 4 will package the first migration from this file.
```

Print the first ~40 lines of the file inline in chat (for review) plus a
`<path>:<total lines>` footer. Full contents live on disk.

## 3f — Review click (AskUserQuestion)

```json
{
  "questions": [
    {
      "question": "Schema review — accept or revise?",
      "header": "Review",
      "multiSelect": false,
      "options": [
        {"label": "Accept — proceed to Phase 4",        "description": "Schema is correct; package into a migration"},
        {"label": "Revise entities (return to Phase 2)", "description": "Add/drop entities or fix a relation"},
        {"label": "Revise types (edit db/schema.sql)",   "description": "Skill will emit a 1-AskUserQuestion per-column-type fix loop"},
        {"label": "Revise indexes (edit db/schema.sql)", "description": "Skill will emit a 1-AskUserQuestion per-index fix loop"}
      ]
    }
  ]
}
```

If the user picks Revise, loop the relevant sub-step; then re-emit and
re-ask. Max 3 revise loops before the skill asks whether to accept-as-is
or escalate to code-implementer handoff (NO DOWNGRADE).

## Verify-criterion

- `db/schema.sql` exists on disk and is non-empty.
- Every FK has a companion index.
- Every non-junction entity has a PK + `created_at`.
- If `ORM = Drizzle` with `STYLE = code-first`: also emit
  `db/schema.ts` alongside `db/schema.sql` (Drizzle introspection artefact;
  see `db-drizzle.md`). For `ORM = SQLx`: record in state that Phase 4's
  migration will also be picked up by `sqlx migrate run`.
- Accept click sets `DDL = "db/schema.sql"` and hands off to Phase 4.
