KeiSeiKit-1.0/skills/schema-design/phase-3-schema.md
Parfii-bot 0be354a920 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

4.9 KiB

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: Userusers, Organizationorganizations; junction OrganizationUserorganization_users).
  2. Columns: infer SQL type from field name + heuristics:
    • *_id, idBIGINT (Postgres/MySQL) / INTEGER (SQLite).
    • *_at, created_at, updated_atTIMESTAMPTZ (PG) / DATETIME (SQLite) / TIMESTAMP (MySQL), DEFAULT now() (PG) / DEFAULT CURRENT_TIMESTAMP (others).
    • email, *_emailTEXT / VARCHAR(320) (RFC 5321 limit) + NOT NULL + CHECK (email LIKE '%@%') (cheap sanity, real validation is app-side).
    • *_count, *_amountINTEGER 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 UNIQUEUNIQUE 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:

-- 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)

{
  "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.