safe-migrate finds risky PostgreSQL migrations before they reach production. It parses SQL, simulates schema changes, and checks the result against a synchronized database snapshot. It never executes migration SQL.
PostgreSQL 14–18 are supported.
Install
With Rust installed:
Prebuilt binaries are available from GitHub Releases. The installer verifies release checksums:
VERSION='v0.9.0'
|
Download install.sh first and run sh install.sh --help to review destination
and target options.
Quick start
sync writes .safe-migrate.cache. Later checks use that snapshot offline.
Run safe-migrate cache inspect to view its provenance and redacted contents.
What it checks
The 29 built-in rules cover:
- blocking locks, table rewrites, constraints, indexes, partitions, and materialized-view refreshes;
- destructive changes, cascades, schema drift, dependency breakage, and migration ordering conflicts;
- grants, policies, disabled triggers, roles, and privilege-sensitive changes;
- missing timeouts, transaction-incompatible operations, dynamic SQL, volatile defaults, and rerun safety.
Run safe-migrate rules for the catalog or inspect one rule directly:
GitHub Actions
Create and protect the safe-migrate-baseline GitHub environment, then run:
This generates a trusted baseline refresh and an offline PR check. Follow the GitHub Action guide to connect the runner and create the first baseline.
Results
| Tier | Meaning | Default command result |
|---|---|---|
Tier1 |
Blocking safety problem | Exit 2 |
Tier2 |
Needs review | Exit 0 |
Tier3 |
Informational guidance | Exit 0 |
Operational failures—such as invalid SQL, configuration, or cache data—exit
1. Every finding includes a stable rule ID, a reason, and remediation:
[HALT] Require concurrent index (require-concurrent-index)
reason : Creating this index can block writes on a large table.
recipe : Use CREATE INDEX CONCURRENTLY outside a transaction.
Use --json for automation or --markdown for review artifacts. See the
CLI and report contract for schemas, confidence, verdicts,
and compatibility guarantees.
Commands
| Command | Purpose |
|---|---|
lint --file migration.sql |
Check one migration. |
lint-chain --dir migrations/ |
Check ordered migrations with state carried forward. |
sync |
Refresh the database baseline. |
cache inspect |
Show baseline provenance and redacted counts. |
rules |
Browse rules and effective settings. |
init github-actions --path migrations/ |
Generate the GitHub integration. |
init cache-key |
Generate a cache-encryption key. |
Run safe-migrate <command> --help for every option.
Database baseline
sync reads PostgreSQL catalogs in a read-only, repeatable-read transaction.
Direct remote connections are rejected; use localhost, a Unix socket, or a
trusted tunnel:
The snapshot reflects the connected role and its session defaults. Choose between a restricted catalog reader and the real migration role based on the accuracy and credential tradeoff described in the Action guide.
The cache contains infrastructure metadata, including schema, roles, privileges, dependencies, and statistics. It contains no credentials, password hashes, or subscription connection strings, but should still be treated as sensitive.
--no-cache is an explicit degraded mode for parser investigation and limited
SQL-only checks. Existing objects are unknown, so confidence is Tainted and
many findings become conservative.
Configuration
Most projects can start with the built-in defaults. Place overrides in
safe-migrate.toml:
= ["public", "auth"]
= 100000
[]
= true
Unknown settings and rule IDs are rejected. safe-migrate rules --json lists
the configuration supported by each rule.
Without a synchronized baseline, the built-in version fallback is deliberately
conservative. Set assume_pg_version only when the target is known to be
PostgreSQL 14–18; for example, assume_pg_version = 170000.
Suppress a reviewed finding with its primary rule ID:
-- safe-migrate: ignore(require-concurrent-index)
(email);
Keep suppressions narrow and explain the reason in the migration review.
Migration timeouts
If the migration runner does not already set timeouts, add them before lock-sensitive changes:
SET lock_timeout = '5s';
SET statement_timeout = '15min';
Keep a positive lock_timeout shorter than a positive statement_timeout.
Rust library
Rust integrations use safe_migrate::api. Load a synchronized baseline when
one is available; otherwise choose explicit conservative analysis.
use ;
use Path;
let config = load_from_file?;
let baseline = load_optional?;
let outcome = analyze?;
if outcome.should_halt
# Ok::
load_optional treats only a missing cache as unavailable; corrupt,
incompatible, or incorrectly encrypted caches remain errors. The API exposes
typed immutable findings, verdicts, evidence, baseline inspection, rule
metadata, and synchronization. Mutable parser, cache, and state-machine
internals are not public. Full API documentation is on
docs.rs.
Embedded applications can call sync_with_secrets with a validated
DatabaseUrl and optional CacheKey. This avoids changing process-wide
environment variables; the CLI continues to read secrets from its environment.
Contributing
See CONTRIBUTING.md for development commands, test suites, and pull-request expectations.
License
Dual-licensed under MIT or Apache-2.0.