add-model · git:20261021.716094f · 2026-10-21 · sha256 eb70f230efc37069
add-model git:20261021.716094fA
Immutable. This exact content never changes and is served at /api/v1/blob/eb70f230efc37069.
---
name: add-model
description: Add or change a database table in a create-bun-api project (SQLite via bun:sqlite) using an append-only migration and a typed repository. Use when asked to add a model, table, column, index, or relation, or to persist new data.
---
# Add or change a model
1. **Append a migration** to `src/db/migrations.ts` with the next id (`000N_<verb>_<table>`). Never edit,
reorder, or delete a migration that may already have run somewhere.
2. **SQL conventions:**
- Ids are `INTEGER PRIMARY KEY AUTOINCREMENT`. Columns are `snake_case`.
- Timestamps: `TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`.
- Foreign keys are enforced. Write `REFERENCES users(id) ON DELETE CASCADE` and index every
foreign key column.
- Use `COLLATE NOCASE` for case-insensitive unique text such as emails.
- One migration may contain several statements.
```ts
{
id: "0002_create_notes",
sql: `
CREATE TABLE notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
title TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
);
CREATE INDEX notes_user_id ON notes (user_id);
`,
},
```
3. **Changing an existing table:** `ALTER TABLE … ADD COLUMN` (nullable or with a `DEFAULT`) and
`ALTER TABLE … RENAME COLUMN` work directly. Dropping a column or changing its type or constraints
needs a table rebuild. Follow [references/table-rebuild.md](references/table-rebuild.md) exactly:
without `disableForeignKeys: true`, the rebuild **silently deletes child rows**.
4. **Repository** (`src/modules/<name>/<name>.repository.ts`):
- Prepare each statement once with `db.query<Row, Params>(sql)`, using `$named` parameters.
Pass the object **without** the `$` (`{ userId }` for `$userId`). The database runs in strict
mode and throws on a missing parameter.
- Map `snake_case` rows to `camelCase` domain objects in one `toX()` function. Never return
secrets such as password hashes.
- For unique violations, check `error.code === "SQLITE_CONSTRAINT_UNIQUE"` and return `null`;
the service then throws `conflict()`.
5. **Test** through the HTTP routes with `createTestApp()`. It gives a fresh in-memory database with
every migration applied. Add a direct test in `src/db/migrations.test.ts` only for migrations
that move data.
6. Run `bun run verify`.