Create a New Migration
Migrations in ncps are derived from the Ent schemas under ent/schema/.
The schema is the only source of truth for DDL; SQL migration files are
generated by cmd/generate-migrations (Atlas, used as a Go library) and
applied at runtime by ncps migrate up (goose-driven).
Never hand-author a .sql file under migrations/<dialect>/ — Atlas
seals the directory with atlas.sum and CI rejects any drift. The only
exception is the --sql-only mode below, which is used for data
backfills and the four-step NOT NULL recipe.
Workflow
Edit the Ent schema. Open
ent/schema/<entity>.goand add or modify the field, edge, index, or annotation. New entities go in a new file underent/schema/.The five codegen invariants apply — see
.agent/skills/ent-schema/SKILL.mdandcmd/ent-lint/for enforcement details.Regenerate the Ent client. This refreshes
ent/migrate/schema.go, whichcmd/generate-migrationsthen diffs against the on-disk migration history.go generate ./ent/... # or task ent:generateCommit the regenerated
ent/tree alongside the schema change — theent-codegen-drift-checkderivation innix flake checkrejects any commit where the tree is stale.Generate per-dialect Atlas migrations. One descriptive snake_case name produces three new
.sqlfiles (sqlite + postgres + mysql) undermigrations/<dialect>/sharing one timestamp prefix, plus regeneratesatlas.sumfor each dialect.go run ./cmd/generate-migrations --name=add_users_table # or task migrations:gen NAME=add_users_tablePlaceholder names (
auto,wip,tmp,todo,temp,test, empty, whitespace) are rejected — supply a descriptive identifier.Review the generated SQL. Open each new file under
migrations/<dialect>/and verify:- The header carries
-- +goose Upand-- +goose Downmarkers. - SQLite files that use
PRAGMA foreign_keys = OFFto drive a temp-table-copy-rename rewrite ALSO carry-- +goose NO TRANSACTIONat the top (goose otherwise wraps the migration in a transaction, which is incompatible with the PRAGMA). - The down migration is a clean inverse of the up — Ent's auto-generated down handles most cases, but verify nontrivial migrations by hand.
- The header carries
Data backfills + four-step NOT NULL recipe. Use
--sql-onlyto emit empty Goose stubs (no Ent diff is run); fill them in manually.go run ./cmd/generate-migrations --sql-only --name=backfill_emails # or task migrations:sql NAME=backfill_emailsThe expand-contract policy applies — see
CLAUDE.mdand the data-model spec for the rationale. Never alter a column in place; split the change across multiple migrations.
Skips and overrides
--skip=sqlite,mysqlskips dialects you don't want regenerated. This is the escape hatch for hand-translated migrations (e.g. the Postgres bridge in §8 of the migrate-to-ent-and-atlas change).--postgres-url=<dsn>/--mysql-url=<dsn>(orNCPS_GEN_POSTGRES_URL/NCPS_GEN_MYSQL_URL) override the dev database URLs. Default URLs target the localhosttest-user@test-dbthatnix run .#depsprovides.
Verifying
task ent:check(orgo generate ./ent/... && git diff --exit-code ./ent/) confirms the Ent client is up to date.go run ./cmd/ent-lint --root .checks schemas against the implemented invariants (A1, A2, A4); A3 and A5 are enforced by code review until wired into the linter.go run ./cmd/atlas-sum-check --root .confirms everyatlas.summatches the directory contents.go test -race -run TestSchemaEquivalence ./migrations/...runs the load-bearing golden test against all three dialects (usesnix run .#depsfor the Postgres + MySQL paths).
All four checks are wired into nix flake check as separate derivations
under nix/checks/flake-module.nix.