KeiSeiKit-1.0/skills/schema-design/phase-3-schema.md
Parfii-bot a4e667de10 KeiSeiKit-public — clean state
Single-commit clean baseline after security scrub of niche-tells,
project codenames, internal jargon, and contributor-email leaks.

Contents:
- 100 Rust crates (_primitives/_rust/)
- 37 agent manifests (_manifests/) + generated specs (_generated/)
- 67 user-invocable skills (skills/)
- 33 hooks (hooks/)
- Composition blocks (_blocks/)
- Documentation (docs/, README.md)
- TS adapter packages (_ts_packages/)
- Assembler (_assembler/)
- Roles (_roles/)
- Templates (_templates/)
- Forgejo CI (.forgejo/)

Author: Denis Parfionovich <info@greendragon.info>

License: see LICENSE.
2026-05-01 12:09:03 +08:00

115 lines
4.9 KiB
Markdown

# 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.