safe-migrate 0.1.0

Lint PostgreSQL migrations against live database statistics to prevent blocking locks
safe-migrate-0.1.0 is not a library.

safe-migrate

safe-migrate is a CI/CD infrastructure tool that prevents PostgreSQL database migrations from causing catastrophic locking queues in production.

It combines static AST (Abstract Syntax Tree) analysis of your SQL migrations with dynamic table statistics from your live database to proactively halt dangerous migrations.

The Problem

Running ALTER TABLE users ADD COLUMN email VARCHAR; on a table with 100 rows is perfectly safe. Running that exact same statement on a table with 100,000,000 rows requires an ACCESS EXCLUSIVE lock that blocks all reads and writes, immediately taking down your application.

Standard linters only look at the code. safe-migrate looks at the code and the size of the tables it affects.

Installation

The easiest way to install safe-migrate on macOS or Linux is via our automated install script, which fetches the correct pre-compiled binary for your system:

curl -fsSL https://raw.githubusercontent.com/dsecurity49/safe-migrate/main/install.sh | bash

Alternative: Cargo (Rust users) If you already have a Rust toolchain installed, you can build from source via crates.io:

cargo install safe-migrate

Quick Start

1. Sync Database Statistics Pull the latest approximate row counts from your database (uses pg_class.reltuples for speed and safety).

export DATABASE_URL="postgres://user:pass@localhost:5432/mydb"
safe-migrate sync

This generates a local .safe-migrate-stats.json cache.

2. Lint Your Migration Evaluate a migration file against the cached stats.

safe-migrate lint --file my_migration.sql

CI/CD Integration (GitHub Actions)

Add this workflow to your repository to automatically lint new migration files on every pull request.

Note: You must set fetch-depth: 0 in the checkout action so Git has enough history to compare branches!

name: Safe Migrate

on:
  pull_request:
    branches: [ "main" ]

jobs:
  lint-migrations:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0 # Required to run git diff against the base branch

      - name: Install safe-migrate
        run: curl -fsSL https://raw.githubusercontent.com/dsecurity49/safe-migrate/main/install.sh | bash

      - name: Sync database stats
        env:
          DATABASE_URL: ${{ secrets.DATABASE_URL }}
        run: safe-migrate sync

      - name: Lint new migrations
        run: |
          # Get list of changed .sql files
          FILES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD -- '*.sql')
          
          if [ -z "$FILES" ]; then
            echo "No SQL migrations changed. Skipping."
            exit 0
          fi
          
          for f in $FILES; do
            echo "Linting $f..."
            safe-migrate lint --file "$f"
          done

Make sure to add your database connection string to your GitHub repository secrets as DATABASE_URL.

Configuration

You can customize lock tiers and row count thresholds by creating a safe-migrate.toml file in your repository root. By default, safe-migrate will flag Tier 1 operations on tables with over 1,000,000 rows.

# safe-migrate.toml

# Global threshold fallback
default_threshold = 500000 

# Override specific rules
[rules.adding-field-with-default]
tier = "Tier1"
threshold = 100000 # Stricter threshold for this specific rule

[rules.require-concurrent-index-creation]
tier = "Tier2"
# Omitting threshold falls back to default_threshold

Risk Tiers

  • Tier 1 (ACCESS EXCLUSIVE): Operations that rewrite the table or block all reads and writes (e.g., adding a field with a default, changing column types). If the threshold is breached, the build halts.
  • Tier 2 (SHARE ROW EXCLUSIVE): Operations that can cause lock queues if not managed properly (e.g., non-concurrent index creation). Emits a warning.
  • Tier 3 (Safe): Standard operations that do not acquire aggressive locks.

Architecture

safe-migrate parses your SQL using a native PostgreSQL Abstract Syntax Tree (AST). It handles quotes, complex schemas, and multi-statement transactions natively without relying on brittle regular expressions or string splitting.