Documentation Index
Fetch the complete documentation index at: https://www.pgschema.com/llms.txt
Use this file to discover all available pages before exploring further.
pgschema picks migration strategies that minimize downtime and blocking when making schema changes to production PostgreSQL databases.
Index Operations
Adding Indexes
When adding new indexes, pgschema automatically uses CREATE INDEX CONCURRENTLY to avoid blocking table writes:
-- Generated migration
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_users_email_status ON users (email, status DESC);
-- pgschema:wait
SELECT
COALESCE(i.indisvalid, false) as done,
CASE
WHEN p.blocks_total > 0 THEN p.blocks_done * 100 / p.blocks_total
ELSE 0
END as progress
FROM pg_class c
LEFT JOIN pg_index i ON c.oid = i.indexrelid
LEFT JOIN pg_stat_progress_create_index p ON c.oid = p.index_relid
WHERE c.relname = 'idx_users_email_status';
The pgschema:wait directive blocks the schema migration execution, polls the database to monitor progress, displays the progress to the user, and automatically continues when the operation completes. It tracks:
- Completion status: Whether the index is valid and ready
- Progress percentage: Based on
pg_stat_progress_create_index
Modifying Indexes
When changing existing indexes, pgschema uses a safe “create new, drop old” pattern:
-- 1. Create new index with temporary name
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_users_email_pgschema_new ON users (email, status);
-- 2. Wait for completion with progress monitoring
-- pgschema:wait
SELECT
COALESCE(i.indisvalid, false) as done,
CASE
WHEN p.blocks_total > 0 THEN p.blocks_done * 100 / p.blocks_total
ELSE 0
END as progress
FROM pg_class c
LEFT JOIN pg_index i ON c.oid = i.indexrelid
LEFT JOIN pg_stat_progress_create_index p ON c.oid = p.index_relid
WHERE c.relname = 'idx_users_email_pgschema_new';
-- 3. Drop old index (fast operation)
DROP INDEX idx_users_email;
-- 4. Rename new index to final name
ALTER INDEX idx_users_email_pgschema_new RENAME TO idx_users_email;
This approach ensures:
- No downtime during index creation
- Query performance is maintained throughout the migration
- Rollback is possible until the old index is dropped
Constraint Operations
Supported constraint types:
- Check constraints: Data validation rules
- Foreign key constraints: Referential integrity with full options
- NOT NULL constraints: Column nullability (via check constraint pattern)
CHECK Constraints
Constraints are added using PostgreSQL’s NOT VALID pattern:
-- 1. Add constraint without validation (fast)
ALTER TABLE orders
ADD CONSTRAINT check_amount_positive CHECK (amount > 0) NOT VALID;
-- 2. Validate existing data (may take time, but non-blocking)
ALTER TABLE orders VALIDATE CONSTRAINT check_amount_positive;
Foreign Key Constraints
Foreign keys use the same NOT VALID pattern:
ALTER TABLE employees
ADD CONSTRAINT employees_company_fkey
FOREIGN KEY (tenant_id, company_id)
REFERENCES companies (tenant_id, company_id)
ON UPDATE CASCADE ON DELETE RESTRICT DEFERRABLE NOT VALID;
ALTER TABLE employees VALIDATE CONSTRAINT employees_company_fkey;
NOT NULL Constraints
Adding NOT NULL constraints uses a three-step process:
-- 1. Add check constraint (non-blocking)
ALTER TABLE users ADD CONSTRAINT email_not_null CHECK (email IS NOT NULL) NOT VALID;
-- 2. Validate constraint (ensures data compliance)
ALTER TABLE users VALIDATE CONSTRAINT email_not_null;
-- 3. Set NOT NULL (fast, since data is already validated)
ALTER TABLE users ALTER COLUMN email SET NOT NULL;
Examples
Adding a Multi-Column Index
Schema change:
-- old.sql
CREATE TABLE users (id int, email text, status text);
-- new.sql
CREATE TABLE users (id int, email text, status text);
CREATE INDEX idx_users_email_status ON users (email, status DESC);
Generated migration:
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_users_email_status ON users (email, status DESC);
-- pgschema:wait
SELECT
COALESCE(i.indisvalid, false) as done,
CASE
WHEN p.blocks_total > 0 THEN p.blocks_done * 100 / p.blocks_total
ELSE 0
END as progress
FROM pg_class c
LEFT JOIN pg_index i ON c.oid = i.indexrelid
LEFT JOIN pg_stat_progress_create_index p ON c.oid = p.index_relid
WHERE c.relname = 'idx_users_email_status';
Adding a Foreign Key Constraint
Schema change:
-- old.sql
CREATE TABLE orders (id int, customer_id int);
-- new.sql
CREATE TABLE orders (
id int,
customer_id int,
CONSTRAINT orders_customer_fkey FOREIGN KEY (customer_id) REFERENCES customers(id)
);
Generated migration:
ALTER TABLE orders
ADD CONSTRAINT orders_customer_fkey
FOREIGN KEY (customer_id) REFERENCES customers(id) NOT VALID;
ALTER TABLE orders VALIDATE CONSTRAINT orders_customer_fkey;
For more examples, see the test cases in testdata/diff/online which demonstrate online DDL patterns for various PostgreSQL schema operations.