PG Schema and Migrations Guide
Use this guide when creating or reviewing database schema changes, migrations, or table structures with @faasjs/pg.
Applicable Scenarios
- Creating or modifying a migration
- Changing tables, columns, indexes, or constraints
- Deciding whether a schema change should use builder helpers or raw SQL
- Reviewing rollback expectations
Default Workflow
- Create a timestamped
.tsmigration file, usually withfaasjs-pg new <name>. - Implement
up(builder)withSchemaBuilderandTableBuilderhelpers first. - Implement
down(builder)for rollback when practical. - Run
faasjs-pg statusfrom the project root to inspect migration history, then usefaasjs-pg migrate,faasjs-pg up, orfaasjs-pg downfor the execution path you need. - Keep migration files in
src/db/migrationsunless you intentionally reconfigure tooling, because both the CLI andPgVitestPlugin()look there by default. - Keep related DDL in one builder run so it stays transactional.
- Fall back to
raw()only for SQL the current helpers do not support.
Minimal Example
import type { SchemaBuilder } from '@faasjs/pg'
export function up(builder: SchemaBuilder) {
builder.createTable('users', (table) => {
table.number('id').primary()
table.string('name')
table.jsonb('metadata').defaultTo('{}')
table.timestamps()
table.index('name')
})
}
export function down(builder: SchemaBuilder) {
builder.dropTable('users')
}
Rules
1. Keep migration filenames lexically sortable
- Migration files should remain timestamp-based and sortable by filename.
- Prefer the generated
faasjs-pg new <name>naming pattern,<timestamp>-<name>.ts, unless there is a strong reason not to. - Prefer hyphenated names such as
create-usersso generated filenames stay readable and match the CLI separator. - Avoid custom naming schemes that break lexical ordering.
2. Prefer builder helpers over handwritten DDL
- Use
createTable,alterTable,renameTable,dropTable, andTableBuildercolumn helpers first. - Use
specificType(...)when the schema needs a PostgreSQL type not covered by a built-in helper. - Use raw DDL only for unsupported features or carefully scoped one-off statements.
3. Preserve transactional schema execution
SchemaBuilder.run()executes accumulated statements in a single transaction.- Write migrations assuming the batch should succeed or fail as one unit.
- Do not split one logical schema change across unrelated builder runs unless partial application is intentional.
4. Keep migrations deterministic and reversible
upanddownshould be direct, readable descriptions of the schema transition.- Avoid time-sensitive or environment-sensitive SQL inside migrations unless it is explicitly required.
- Do not use defensive
IF EXISTSorIF NOT EXISTSDDL clauses in migrations; let unexpected schema state fail immediately so drift is caught during migration. - Prefer reversible changes when practical so
down()can restore the previous state.
5. Keep migration history semantics stable
faasjs_pg_migrationsis the source of migration history.migrate()applies all pending files,up()applies the next pending file, anddown()rolls back the latest recorded file.- Treat those behaviors as the default mental model for app code, tooling, and troubleshooting.
6. Keep the execution path obvious
- Keep migrations in the project-root
src/db/migrationsfolder unless project tooling is configured otherwise. - Use
faasjs-pg statusto inspect history,faasjs-pg migrateto apply all pending files,faasjs-pg upfor the next file, andfaasjs-pg downfor the latest rollback. - If a project customizes the folder or wrapper commands, document that override explicitly in the project README or contributor guide.
- Keep migrations focused on application-owned schema. Framework-managed tables such as
faasjs_jobsare initialized by their owning package and should not be recreated in app migrations.
See Also
- PG Table Types Guide — updating
Tablesafter migration changes - PG Query Builder and Raw SQL Guide — querying the tables you create
- PG Testing Guide — testing with
PgVitestPlugin()
Review Checklist
- the migration file name remains timestamp-sorted
upanddownare both present when rollback is practical- the
status/migrate/up/downexecution flow is obvious for the project - builder helpers are used before raw DDL
- raw DDL does not hide drift with
IF EXISTSorIF NOT EXISTS - schema changes expect
SchemaBuilder.run()to be atomic - risky schema changes are covered by focused migration or integration tests
- framework-owned tables are not duplicated in app migrations