pgpm-changes
Authoring Database Changes with PGPM
Create safe, reversible database changes using pgpm's three-file pattern. Every change has deploy, revert, and verify scripts.
When to Apply
Use this skill when:
- Adding tables, functions, triggers, or indexes
- Creating database migrations
- Modifying existing schema
- Organizing database changes in a pgpm module
The Three-File Pattern
Every database change consists of three files:
| File | Purpose |
|---|---|
deploy/<change>.sql |
Creates the object |
revert/<change>.sql |
Removes the object |
verify/<change>.sql |
Confirms deployment |
Adding a Change
pgpm add schemas/pets/tables/pets --requires schemas/pets
This creates:
deploy/schemas/pets/tables/pets.sql
revert/schemas/pets/tables/pets.sql
verify/schemas/pets/tables/pets.sql
And updates pgpm.plan:
schemas/pets/tables/pets [schemas/pets] 2025-11-14T00:00:00Z Author <author@example.com>
Writing Deploy Scripts
Deploy scripts create database objects. Use CREATE, not CREATE OR REPLACE (pgpm is deterministic).
deploy/schemas/pets/tables/pets.sql:
-- Deploy: schemas/pets/tables/pets
-- requires: schemas/pets
CREATE TABLE pets.pets (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name TEXT NOT NULL,
breed TEXT,
owner_id UUID,
created_at TIMESTAMPTZ DEFAULT NOW()
);
Important: Never use CREATE OR REPLACE unless absolutely necessary. pgpm tracks what's deployed and ensures idempotency through its migration system.
Writing Revert Scripts
Revert scripts undo the deploy. Must leave database in pre-deploy state.
revert/schemas/pets/tables/pets.sql:
-- Revert: schemas/pets/tables/pets
DROP TABLE IF EXISTS pets.pets;
Writing Verify Scripts
Verify scripts confirm deployment succeeded. Use DO blocks that raise exceptions on failure.
verify/schemas/pets/tables/pets.sql:
-- Verify: schemas/pets/tables/pets
DO $$
BEGIN
PERFORM 1 FROM pg_tables
WHERE schemaname = 'pets' AND tablename = 'pets';
IF NOT FOUND THEN
RAISE EXCEPTION 'Table pets.pets does not exist';
END IF;
END $$;
Nested Paths
Organize changes hierarchically using nested paths:
schemas/
└── app/
├── schema.sql
├── tables/
│ └── users/
│ ├── table.sql
│ └── indexes/
│ └── email.sql
├── functions/
│ └── create_user.sql
└── triggers/
└── updated_at.sql
Add changes with full paths:
pgpm add schemas/app/schema
pgpm add schemas/app/tables/users/table --requires schemas/app/schema
pgpm add schemas/app/tables/users/indexes/email --requires schemas/app/tables/users/table
pgpm add schemas/app/functions/create_user --requires schemas/app/tables/users/table
Key insight: Deployment order follows the plan file, not directory structure. Nested paths are for organization only.
Plan File Format
The pgpm.plan file tracks all changes:
%syntax-version=1.0.0
%project=pets
%uri=pets
schemas/pets 2025-11-14T00:00:00Z Author <author@example.com>
schemas/pets/tables/pets [schemas/pets] 2025-11-14T00:00:00Z Author <author@example.com>
schemas/pets/tables/pets/indexes/name [schemas/pets/tables/pets] 2025-11-14T00:00:00Z Author <author@example.com>
Format: change_name [dependencies] timestamp author <email> # optional note
Two Workflows
Incremental (Development)
Add changes one at a time:
pgpm add schemas/pets --requires uuid-ossp
pgpm add schemas/pets/tables/pets --requires schemas/pets
Plan file updates automatically with each pgpm add.
Pre-Production (Batch)
Write all SQL files first, then generate plan:
# Write deploy/revert/verify files manually
# Then generate plan from requires comments:
pgpm plan
pgpm plan reads -- requires: comments from deploy files and generates the plan.
Common Change Types
Schema
pgpm add schemas/app
-- deploy/schemas/app.sql
CREATE SCHEMA app;
-- revert/schemas/app.sql
DROP SCHEMA IF EXISTS app CASCADE;
-- verify/schemas/app.sql
DO $$ BEGIN
PERFORM 1 FROM information_schema.schemata WHERE schema_name = 'app';
IF NOT FOUND THEN RAISE EXCEPTION 'Schema app does not exist'; END IF;
END $$;
Table
pgpm add schemas/app/tables/users --requires schemas/app
-- deploy/schemas/app/tables/users.sql
CREATE TABLE app.users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
email TEXT UNIQUE NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- revert/schemas/app/tables/users.sql
DROP TABLE IF EXISTS app.users;
-- verify/schemas/app/tables/users.sql
DO $$ BEGIN
PERFORM 1 FROM pg_tables WHERE schemaname = 'app' AND tablename = 'users';
IF NOT FOUND THEN RAISE EXCEPTION 'Table app.users does not exist'; END IF;
END $$;
Function
pgpm add schemas/app/functions/get_user --requires schemas/app/tables/users
-- deploy/schemas/app/functions/get_user.sql
CREATE FUNCTION app.get_user(user_id UUID)
RETURNS app.users AS $$
SELECT * FROM app.users WHERE id = user_id;
$$ LANGUAGE sql STABLE;
-- revert/schemas/app/functions/get_user.sql
DROP FUNCTION IF EXISTS app.get_user(UUID);
-- verify/schemas/app/functions/get_user.sql
DO $$ BEGIN
PERFORM 1 FROM pg_proc WHERE proname = 'get_user';
IF NOT FOUND THEN RAISE EXCEPTION 'Function get_user does not exist'; END IF;
END $$;
Index
pgpm add schemas/app/tables/users/indexes/email --requires schemas/app/tables/users
-- deploy/schemas/app/tables/users/indexes/email.sql
CREATE INDEX idx_users_email ON app.users(email);
-- revert/schemas/app/tables/users/indexes/email.sql
DROP INDEX IF EXISTS app.idx_users_email;
-- verify/schemas/app/tables/users/indexes/email.sql
DO $$ BEGIN
PERFORM 1 FROM pg_indexes WHERE indexname = 'idx_users_email';
IF NOT FOUND THEN RAISE EXCEPTION 'Index idx_users_email does not exist'; END IF;
END $$;
Deploy and Verify
# Deploy to database
pgpm deploy --database myapp_dev --createdb --yes
# Verify deployment
pgpm verify --database myapp_dev
References
- Related skill:
pgpm-workspacefor workspace setup - Related skill:
pgpm-dependenciesfor cross-module dependencies - Related skill:
pgpm-testingfor testing database changes
More from constructive-io/constructive-skills
drizzle-orm
Drizzle ORM patterns for PostgreSQL schema design and queries. Use when asked to "design Drizzle schema", "write Drizzle queries", "set up Drizzle ORM", or when building type-safe database layers.
21planning-blueprinting
In-repo planning and specification system for software projects. Use when asked to "create a plan", "write a spec", "document a proposal", "blueprint a feature", or when doing architectural planning work.
20pgsql-parser-testing
Test the pgsql-parser repository (SQL parser/deparser). Use when working in the pgsql-parser repo, fixing deparser issues, running parser tests, or validating SQL round-trips. Scoped specifically to the constructive-io/pgsql-parser repository.
18constructive-graphql-codegen
Generate type-safe React Query hooks, Prisma-like ORM client, or inquirerer-based CLI from GraphQL endpoints, schema files/directories, databases, or PGPM modules using @constructive-io/graphql-codegen. Also generates documentation (README, AGENTS.md, skills/, mcp.json). Use when asked to "generate GraphQL hooks", "generate ORM", "generate CLI", "set up codegen", "generate docs", "generate skills", "export schema", or when implementing data fetching for a PostGraphile backend.
17constructive-server-config
Configure and run the Constructive GraphQL server (cnc server), GraphiQL explorer (cnc explorer), and code generation (cnc codegen). Use when asked to "start the server", "run cnc server", "start GraphQL API", "run GraphiQL", "configure API routing", "generate types", or when working with the Constructive CLI and PostGraphile.
17constructive-boilerplate-nextjs-app
Set up and develop with the Constructive App frontend boilerplate — a Next.js application with authentication, organization management, invites, members, and a GraphQL SDK. Use when scaffolding a new Constructive frontend application from the boilerplate.
17