DB Migration
Instructions
Create or update the database schema as goose SQL migrations based on docs/entity_model.md,
and regenerate the sqlc Go code.
goose does not diff a schema — every migration is written by hand as SQL. That is intended:
the migration file is the reviewed change. Each file has an -- +goose Up and an
-- +goose Down section.
DO NOT
- Edit a migration that has already been applied anywhere (merged to
main, or run against dev) — add a new migration instead
- Drop or rename existing tables or columns without explicit user confirmation (a
DROP in the -- +goose Down section of a new migration is expected and fine)
- Edit anything under
internal/db/ — it is generated by sqlc generate
- Skip relations defined in the entity model — every relation becomes a foreign key
- Use
serial / bigserial IDs unless the entity model explicitly requires them (use uuid with DEFAULT gen_random_uuid())
Nexa Rules Gate
Read and follow ${CLAUDE_PLUGIN_ROOT}/shared/readiness/NEXA_RULES_GATE.md.
Worktree Gate
Read and follow ${CLAUDE_PLUGIN_ROOT}/shared/readiness/WORKTREE_GATE.md.
Locations
db/migrations/NNNNN_<name>.sql # goose migrations (schema source for sqlc)
db/migrations/embed.go # embeds the migrations, exposes Up(ctx, dsn)
db/queries/<entity>.sql # sqlc queries
sqlc.yaml
internal/db/ # generated — committed, never edited
Example Migration
-- +goose Up
CREATE TABLE room_type (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
name varchar(50) NOT NULL UNIQUE,
description varchar(500),
capacity integer NOT NULL CHECK (capacity > 0),
price numeric(10,2) NOT NULL
);
CREATE TABLE room (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
number varchar(10) NOT NULL UNIQUE,
room_type_id uuid NOT NULL REFERENCES room_type (id)
);
CREATE INDEX room_room_type_id_idx ON room (room_type_id);
-- +goose Down
DROP TABLE room;
DROP TABLE room_type;
Example Queries
-- name: GetRoomType :one
SELECT * FROM room_type WHERE id = $1;
-- name: ListRoomTypes :many
SELECT * FROM room_type ORDER BY name;
Entity Model → SQL Mapping
| Entity model |
PostgreSQL |
| ID |
uuid PRIMARY KEY DEFAULT gen_random_uuid() |
| String with max length |
varchar(N) |
| Text without limit |
text |
| Integer / Long |
integer / bigint |
| Decimal / money |
numeric(P,S) — never float for money |
| Boolean |
boolean NOT NULL DEFAULT false (or the model's default) |
| Date / DateTime |
date / timestamptz |
| Enum |
text + CHECK (col IN (...)) |
| Required |
NOT NULL |
| Unique |
UNIQUE |
| Min / max value |
CHECK (...) |
| Many-to-one |
<entity>_id uuid NOT NULL REFERENCES <entity> (id) + index |
| Many-to-many |
join table with composite primary key of both foreign keys |
Table and column names are snake_case, singular table names.
Workflow
- Read
docs/entity_model.md
- Read the existing migrations in
db/migrations/ (if any) to know the current schema
- Bootstrap once, if missing:
sqlc.yaml:version: "2"
sql:
- engine: "postgresql"
schema: "db/migrations"
queries: "db/queries"
gen:
go:
package: "db"
out: "internal/db"
sql_package: "pgx/v5"
emit_interface: true
db/migrations/embed.go:package migrations
import (
"context"
"database/sql"
"embed"
_ "github.com/jackc/pgx/v5/stdlib"
"github.com/pressly/goose/v3"
)
//go:embed *.sql
var FS embed.FS
// Up applies every pending migration to the database at dsn.
func Up(ctx context.Context, dsn string) error {
db, err := sql.Open("pgx", dsn)
if err != nil {
return err
}
defer db.Close()
p, err := goose.NewProvider(goose.DialectPostgres, db, FS)
if err != nil {
return err
}
_, err = p.Up(ctx)
return err
}
db/migrations/migrations_integration_test.go — the round-trip check (build tag
integration): start a testcontainers-go Postgres container, run Up, then
goose.NewProvider(...).DownTo(ctx, 0), then Up again; fail on any error. This proves
every Down section reverses its Up.
- Tools:
go get -tool github.com/pressly/goose/v3/cmd/goose; verify sqlc version (install
the binary per the sqlc docs if missing). Use context7 to confirm goose and sqlc
configuration for the installed versions.
- Create the migration file:
go tool goose -dir db/migrations create <descriptive_name> sql
- Write the
Up section from the entity model (see the mapping table) and a Down section that
reverses it exactly, in reverse dependency order
- Ensure relations match the entity model (foreign keys, join tables,
ON DELETE behaviour only
when the model states it)
- For each new table, add
db/queries/<entity>.sql with a get-by-id and a list query if the file
does not exist. Use-case-specific queries are added later by /implement
- Run
sqlc generate
- Validate the migration:
go test -tags=integration ./db/migrations/ passes — the migrations apply to a fresh
container and survive a full down/up round trip (Docker must be running)
go build ./... succeeds against the regenerated internal/db/
- Every entity in the entity model has a table; every attribute has a column with the mapped
type and constraints; every relation has a foreign key
- Read the migration SQL once more for a destructive statement in
Up — if there is one,
confirm with the user
Verification
The skill is complete when:
go test -tags=integration ./db/migrations/ exits 0
sqlc generate exits 0 and go build ./... exits 0
internal/db/ changes are staged with the migration (generated code is committed)
- No previously applied migration file was modified (
git diff origin/main -- db/migrations/ shows only added files)
1---2name: db-migration3description: Creates goose SQL migrations and sqlc queries from the entity model, then regenerates the type-safe Go database code. Use when the user asks to "create a migration", "update the schema", "set up database tables", "write a goose migration", "add sqlc queries", or mentions schema migration, DB migration, database versioning, goose, or sqlc.4---56# DB Migration78## Instructions910Create or update the database schema as goose SQL migrations based on `docs/entity_model.md`,11and regenerate the sqlc Go code.1213goose does not diff a schema — every migration is written by hand as SQL. That is intended:14the migration file *is* the reviewed change. Each file has an `-- +goose Up` and an15`-- +goose Down` section.1617## DO NOT1819- Edit a migration that has already been applied anywhere (merged to `main`, or run against dev) — add a new migration instead20- Drop or rename existing tables or columns without explicit user confirmation (a `DROP` in the `-- +goose Down` section of a new migration is expected and fine)21- Edit anything under `internal/db/` — it is generated by `sqlc generate`22- Skip relations defined in the entity model — every relation becomes a foreign key23- Use `serial` / `bigserial` IDs unless the entity model explicitly requires them (use `uuid` with `DEFAULT gen_random_uuid()`)2425## Nexa Rules Gate2627Read and follow `${CLAUDE_PLUGIN_ROOT}/shared/readiness/NEXA_RULES_GATE.md`.2829## Worktree Gate3031Read and follow `${CLAUDE_PLUGIN_ROOT}/shared/readiness/WORKTREE_GATE.md`.3233## Locations3435```36db/migrations/NNNNN_<name>.sql # goose migrations (schema source for sqlc)37db/migrations/embed.go # embeds the migrations, exposes Up(ctx, dsn)38db/queries/<entity>.sql # sqlc queries39sqlc.yaml40internal/db/ # generated — committed, never edited41```4243## Example Migration4445```sql46-- +goose Up47CREATE TABLE room_type (48 id uuid PRIMARY KEY DEFAULT gen_random_uuid(),49 name varchar(50) NOT NULL UNIQUE,50 description varchar(500),51 capacity integer NOT NULL CHECK (capacity > 0),52 price numeric(10,2) NOT NULL53);5455CREATE TABLE room (56 id uuid PRIMARY KEY DEFAULT gen_random_uuid(),57 number varchar(10) NOT NULL UNIQUE,58 room_type_id uuid NOT NULL REFERENCES room_type (id)59);6061CREATE INDEX room_room_type_id_idx ON room (room_type_id);6263-- +goose Down64DROP TABLE room;65DROP TABLE room_type;66```6768## Example Queries6970```sql71-- name: GetRoomType :one72SELECT * FROM room_type WHERE id = $1;7374-- name: ListRoomTypes :many75SELECT * FROM room_type ORDER BY name;76```7778## Entity Model → SQL Mapping7980| Entity model | PostgreSQL |81|---|---|82| ID | `uuid PRIMARY KEY DEFAULT gen_random_uuid()` |83| String with max length | `varchar(N)` |84| Text without limit | `text` |85| Integer / Long | `integer` / `bigint` |86| Decimal / money | `numeric(P,S)` — never `float` for money |87| Boolean | `boolean NOT NULL DEFAULT false` (or the model's default) |88| Date / DateTime | `date` / `timestamptz` |89| Enum | `text` + `CHECK (col IN (...))` |90| Required | `NOT NULL` |91| Unique | `UNIQUE` |92| Min / max value | `CHECK (...)` |93| Many-to-one | `<entity>_id uuid NOT NULL REFERENCES <entity> (id)` + index |94| Many-to-many | join table with composite primary key of both foreign keys |9596Table and column names are `snake_case`, singular table names.9798## Workflow991001. Read `docs/entity_model.md`1012. Read the existing migrations in `db/migrations/` (if any) to know the current schema1023. Bootstrap once, if missing:103 - `sqlc.yaml`:104 ```yaml105 version: "2"106 sql:107 - engine: "postgresql"108 schema: "db/migrations"109 queries: "db/queries"110 gen:111 go:112 package: "db"113 out: "internal/db"114 sql_package: "pgx/v5"115 emit_interface: true116 ```117 - `db/migrations/embed.go`:118 ```go119 package migrations120121 import (122 "context"123 "database/sql"124 "embed"125126 _ "github.com/jackc/pgx/v5/stdlib"127 "github.com/pressly/goose/v3"128 )129130 //go:embed *.sql131 var FS embed.FS132133 // Up applies every pending migration to the database at dsn.134 func Up(ctx context.Context, dsn string) error {135 db, err := sql.Open("pgx", dsn)136 if err != nil {137 return err138 }139 defer db.Close()140 p, err := goose.NewProvider(goose.DialectPostgres, db, FS)141 if err != nil {142 return err143 }144 _, err = p.Up(ctx)145 return err146 }147 ```148 - `db/migrations/migrations_integration_test.go` — the round-trip check (build tag149 `integration`): start a testcontainers-go Postgres container, run `Up`, then150 `goose.NewProvider(...).DownTo(ctx, 0)`, then `Up` again; fail on any error. This proves151 every `Down` section reverses its `Up`.152 - Tools: `go get -tool github.com/pressly/goose/v3/cmd/goose`; verify `sqlc version` (install153 the binary per the sqlc docs if missing). Use context7 to confirm goose and sqlc154 configuration for the installed versions.1554. Create the migration file: `go tool goose -dir db/migrations create <descriptive_name> sql`1565. Write the `Up` section from the entity model (see the mapping table) and a `Down` section that157 reverses it exactly, in reverse dependency order1586. Ensure relations match the entity model (foreign keys, join tables, `ON DELETE` behaviour only159 when the model states it)1607. For each new table, add `db/queries/<entity>.sql` with a get-by-id and a list query if the file161 does not exist. Use-case-specific queries are added later by `/implement`1628. Run `sqlc generate`1639. Validate the migration:164 - `go test -tags=integration ./db/migrations/` passes — the migrations apply to a fresh165 container and survive a full down/up round trip (Docker must be running)166 - `go build ./...` succeeds against the regenerated `internal/db/`167 - Every entity in the entity model has a table; every attribute has a column with the mapped168 type and constraints; every relation has a foreign key169 - Read the migration SQL once more for a destructive statement in `Up` — if there is one,170 confirm with the user171172## Verification173174The skill is complete when:175176- `go test -tags=integration ./db/migrations/` exits 0177- `sqlc generate` exits 0 and `go build ./...` exits 0178- `internal/db/` changes are staged with the migration (generated code is committed)179- No previously applied migration file was modified (`git diff origin/main -- db/migrations/` shows only added files)