pg-delta generates migration scripts that are both safe to execute and easy to review. This is achieved through a sophisticated sorting algorithm that balances database constraints with human readability.
When generating a migration script, pg-delta organizes changes to satisfy two competing goals:
- Correctness: Statements execute in the right order for PostgreSQL (e.g., create a table before its indexes)
- Readability: Related changes are grouped together for easier code review
The sorting engine uses a two-pass strategy:
- Logical Organization: Groups related changes together (by schema, by table, etc.)
- Dependency Resolution: Adjusts ordering to satisfy PostgreSQL's requirements
This means your migration scripts will have all changes to public.users grouped together, while still ensuring that tables are created before their foreign keys reference them.
Migrations are organized into two distinct phases:
Runs first, in reverse dependency order:
- Drops dependents before their dependencies
- Example: Drop a foreign key before dropping the referenced table
Runs second, in forward dependency order:
- Creates dependencies before their dependents
- Example: Create a role before assigning it as a table owner
pg-delta automatically handles dependencies from multiple sources:
| Source | Description | Example |
|---|---|---|
| Database Catalog | Dependencies tracked by PostgreSQL (pg_depend) |
Views depending on tables |
| Explicit Requirements | Dependencies declared in object definitions | Column referencing a type |
| Logical Rules | Business logic requirements | Default privileges before table creation |
Real-world schemas sometimes contain circular dependencies (e.g., two tables with mutual foreign key references).
pg-delta handles this by:
- Detecting the cycle
- Identifying if any constraint can be deferred (created separately via
ALTER TABLE) - Breaking the cycle by separating the constraint
- Re-sorting the changes
If a cycle cannot be broken (only hard dependencies remain), pg-delta throws a detailed error explaining the cycle.
Input changes:
CREATE TABLE posts(owner:admin)CREATE ROLE admin
Output (reordered):
CREATE ROLE adminCREATE TABLE posts
The role must exist before it can own a table.
Input changes:
CREATE TABLE usersCREATE TABLE postsCREATE INDEX users_email_idx ON usersCREATE INDEX posts_author_idx ON posts
Output (grouped by table):
CREATE TABLE usersCREATE INDEX users_email_idx ON usersCREATE TABLE postsCREATE INDEX posts_author_idx ON posts
Related objects are kept together for easier review.
Input changes:
CREATE TABLE usersALTER DEFAULT PRIVILEGES ... GRANT SELECT ON TABLES
Output (reordered):
ALTER DEFAULT PRIVILEGES ... GRANT SELECT ON TABLESCREATE TABLE users
Default privileges must be set before tables are created for them to inherit the correct permissions.
pg-delta uses stable string identifiers to track objects across environments (since PostgreSQL OIDs change between databases):
| Object Type | Identifier Format | Example |
|---|---|---|
| Schema Object | type:schema.name |
table:public.users |
| Sub-entity | type:schema.parent.name |
column:public.users.email |
| Metadata | scope:target |
comment:public.users |
These identifiers ensure consistent dependency tracking regardless of the underlying database.