PostgreSQL 17 runners are live

A GitHub App for PostgreSQL migrations

Rehearse every migration on a database that looks like production.

  • PostgreSQL 14
  • PostgreSQL 15
  • PostgreSQL 16
  • PostgreSQL 17
  • golang-migrate
  • Flyway
  • dbmate
  • Prisma

Run it for real

Measured, not guessed. Linters read your SQL. ShadowDB runs it on a throwaway PostgreSQL full of rows and records what the database actually did.

Locks, per statement. Which lock, on which table, for how long.

ACCESS EXCLUSIVE SHARE

Seeded data. Rewrites take as long as they would in production.

Seeding31/48 tables
  • Replayed 37 base migrations
  • Respected foreign keys
  • Running migration…

Isolated runner. No network, no credentials, gone after the run.

net: nonesecrets: nonelifetime: one run

Schema diff. What changed, object by object, not DDL.

TABLE orders
  id       bigint- total    numeric+ total    numeric(12,2)  status   text+ INDEX    orders_status_idx

Catch it in review

Answers before anyone merges. Every pull request that touches a migration gets a rehearsal, a verdict and a report your reviewers can read.

  • On the pull request

    A GitHub check with the verdict, so a risky migration can't be merged by accident.

  • Points at the line

    Every finding names the statement and the rule it broke.

  • Shows the safer way

    Each rule explains the risk and what to write instead. Re-run after you push.

shadowdb — sandbox · no network · pg16

● pull_request acme/api#482 · 1 migration changed · pg16 → replaying 37 base migrations from main …………… 1.8s → seeding 1,000,000 synthetic rows into orders ……… 3.9s → running 0038_add_status.up.sql statement by statement ALTER TABLE orders ALTER COLUMN total TYPE numeric(12,2); ✗ ACCESS EXCLUSIVE on orders held 9.2s · table rewritten CREATE INDEX orders_status_idx ON orders (status); ⚠ SHARE lock blocks writes for 3.1s · SDB201 verdict: BLOCKED 2 findings · schema diff ready · check sent to PR

Checks · #482

  • ShadowDB / migrations1 critical · 1 high
  • ci / test2m 14s
  • ci / lint41s

SDB201 · suggestedno write lock

CREATE INDEX
CREATE INDEX CONCURRENTLY
  orders_status_idx
  ON orders (status);

Know the failure modes

Knows the ways a migration hurts. Each rule is checked against what really happened in the run, with a severity you can tune per project.

Thirty-two rules.
Zero guesses.

From dropped tables to unvalidated constraints, caught before merge.

SDB101Table dropped[critical]
SDB102Column dropped[high]
SDB103Schema dropped[critical]
SDB104Table truncated[critical]
SDB105UPDATE or DELETE without WHERE[medium]
SDB111Table renamed[high]
SDB112Column renamed[high]
SDB113View or function changed under clients[medium]
SDB201Index built without CONCURRENTLY[high]
SDB202Index dropped without CONCURRENTLY[medium]
SDB301Column type change rewrites the table[high]
SDB302Column added with a volatile default[high]
SDB303Table switched between LOGGED and UNLOGGED[high]
SDB304Stored generated column added[high]
SDB305Identity column added to an existing table[high]
SDB401CHECK constraint added without NOT VALID[medium]
SDB402Foreign key added without NOT VALID[high]
SDB501Unique constraint added to existing data[high]
SDB502SET NOT NULL on an existing column[high]
SDB503NOT NULL column added without a default[high]
SDB601Statement can't run inside a transaction[critical]
SDB602Nested BEGIN/COMMIT in a wrapped file[high]
SDB603No lock_timeout before a blocking lock[medium]
SDB701Already-merged migration edited[high]
SDB702Migration version out of order[medium]
SDB801Table rewritten during execution[high]
SDB802ACCESS EXCLUSIVE lock held[high]
SDB803Slow statement[medium]
SDB804Statement failed[critical]
SDB901Syntax error[critical]
SDB902Dynamic SQL not analysed statically[info]
SDB903Invalid suppression comment[low]

32

rules in 11 categories, from destructive changes and rewrites to locks, transactions and migration history

14–17

PostgreSQL majors, each with its own runner image

0

credentials or network access inside the sandbox

Changelog

What shipped.

Subscribe via RSS
  1. Every rule explains itself

    The rules reference now says, for each rule, its category and default severity, why the statement is risky on a live database, and what to write instead.

  2. Use a production schema snapshot as the base

    Upload a schema-only pg_dump and runs start from it instead of replaying every base migration. Data statements are refused at upload, so no production rows are stored.

  3. Run authors, branch status and notifications

    Runs show the commit author and message, each branch and pull request shows its latest verdict, and results can be sent to Slack or a webhook.

  4. API tokens and API-triggered runs

    Create API tokens to start runs from CI or scripts, and optionally have ShadowDB post a comment on the pull request alongside the check.

  5. PostgreSQL 14 to 17, new rules, audit log

    Runner images for every PostgreSQL major from 14 to 17, more rules, an organization audit log, and a downloadable run report.

Stop finding out in production.

Install the GitHub App, pick a repository, and your next migration PR gets a rehearsal.