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.
With Rust installed:
cargo install safe-migrate --lockedPrebuilt binaries are available from GitHub Releases. The installer verifies release checksums:
VERSION='v0.8.0'
curl -fsSL "https://raw.githubusercontent.com/dsecurity49/safe-migrate/${VERSION}/install.sh" |
bash -s -- --version "${VERSION}"Download install.sh first and run sh install.sh --help to review destination
and target options.
export DATABASE_URL='postgres://readonly_user@localhost:5432/app'
safe-migrate sync
safe-migrate lint-chain --dir migrationssync writes .safe-migrate.cache. Later checks use that snapshot offline.
Run safe-migrate cache inspect to view its provenance and redacted contents.
The 28 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:
safe-migrate rules --rule require-concurrent-indexCreate the safe-migrate-baseline GitHub environment, then run:
safe-migrate init github-actions --path migrations --configure-secretsThis generates a trusted baseline refresh and an offline PR check. Follow the GitHub Action guide to connect the runner and create the first baseline.
| 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.
| 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.
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:
ssh -N -L 5433:db.internal:5432 bastion
export DATABASE_URL='postgres://readonly_user@localhost:5433/app'
safe-migrate syncThe 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.
Most projects can start with the built-in defaults. Place overrides in
safe-migrate.toml:
schemas = ["public", "auth"]
tier1_threshold_rows = 100000
[rules.missing-idempotency]
disabled = trueUnknown settings and rule IDs are rejected. safe-migrate rules --json lists
the configuration supported by each rule.
Suppress a reviewed finding with its primary rule ID:
-- safe-migrate: ignore(require-concurrent-index)
CREATE INDEX users_email_idx ON users (email);Keep suppressions narrow and explain the reason in the migration review.
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 integrations should use the supported safe_migrate::api façade.
Documentation is published on docs.rs.
See CONTRIBUTING.md for development commands, test suites, and pull-request expectations.
Dual-licensed under MIT or Apache-2.0.