Checking MySQL migrations in CI without a dev database
Most migration-safety tooling wants a database. It starts MySQL in your pipeline, replays the migration against it and reports what would happen. That is the more thorough approach — and for a lot of teams it is also the reason the check never gets added. This page is about the other option: deciding whether a migration is risky from the statement text alone, what that buys you, and exactly what it cannot tell you.
The change that starts the conversation
A column needs to hold emoji, so the table's character set changes:
ALTER TABLE users CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
One line, reviewed in seconds, approved by two people. The MySQL 8.0 manual's
online DDL operations table lists
converting a character set as in place: yes, rebuilds table: yes, permits concurrent DML: no. On a
large, busy users table that is a table rewrite with writes blocked for the duration — and the information was in
the manual the whole time.
The same holds for a handful of other statement classes: ADD FOREIGN KEY (a copy unless
foreign_key_checks is disabled), MODIFY COLUMN when the data type really changes, adding a
FULLTEXT or SPATIAL index, primary-key changes, ENGINE= and ROW_FORMAT=,
and anything destructive — DROP TABLE, DROP COLUMN, TRUNCATE. MySQL 8.0's atomic DDL is
not transactional DDL: a DDL statement implicitly commits
the session's transaction before it runs, so a later ROLLBACK cannot undo one that has already executed.
Two ways to check this in a pull request
| With a database in CI | From the statement text | |
|---|---|---|
| What it can see | your actual schema and column definitions — it replays the change | the statement, and the MySQL rules that apply to it |
| What the job needs | a MySQL instance, commonly an ephemeral Docker container, plus the time to start it | nothing beyond the runner |
| Precision | higher — it can tell you what your column is today | lower on anything that depends on the current definition |
| Still invisible | table size, production traffic, replication lag | all of the above, plus the current schema, the server version and foreign_key_checks |
| Typical failure | the check never gets added, because someone has to own a database in the pipeline | a finding that says "potentially" and needs a human to confirm |
Neither approach tells you how long a rebuild will take on your table. Table size is the most useful input to that question and no static check has it.
What a statement-only check actually reports
Real output from Presift, the tool behind this site, on the migration above:
✖ MG002 ERROR V12__charset.sql:1
ALTER TABLE users: CONVERT TO CHARACTER SET rebuilds the table and blocks concurrent DML
Why: Converting a table's character set rewrites every row (the manual lists it as in place,
rebuilds table, permits concurrent DML: No); writes are blocked for the duration.
Do: Avoid on large/busy tables during traffic. Options: expand/contract; gh-ost /
pt-online-schema-change; or a maintenance window. Assert the expectation explicitly with
ALGORITHM=... so MySQL fails fast if it cannot comply.
Ref: https://dev.mysql.com/doc/refman/8.0/en/innodb-online-ddl-operations.html
Just as important is what it does not report. All three of these pass with no findings:
ALTER TABLE orders ADD INDEX idx_customer_created (customer_id, created_at), ALGORITHM=INPLACE, LOCK=NONE;
ALTER TABLE orders ADD COLUMN referral_code VARCHAR(32) NULL;
CREATE TABLE audit_log (id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, ...) ENGINE=InnoDB;
A change you have decided to make anyway is annotated in the migration itself, with a reason the next reviewer can read:
-- presift: allow MG001 replaced by orders_v2 in migration 0141, verified unused for 30 days
DROP TABLE orders_legacy;
Where the outcome depends on a column's existing definition — MODIFY COLUMN is the common case — the finding
says so and is marked medium confidence rather than claiming certainty it cannot have.
How this compares with the tools you have probably already seen
Written as fairly as we can manage. Each of these does things Presift does not.
| Tool | What it is | MySQL lock/rebuild analysis | Needs a database? | Cost |
|---|---|---|---|---|
| Atlas | schema-as-code, versioned migrations, linting, drift and policy — far broader than a linter | yes, and more thorough than ours: its MySQL analyzers cover the same statement classes, and its dev-database simulation sees your actual schema | yes — atlas migrate lint takes a --dev-url, usually an ephemeral Docker instance | its lint command moved out of the free plan in v0.38 (2025-10-28); Pro is $9 per developer per month plus $59 per month per CI project |
| Skeema | declarative pure-SQL schema management for MySQL and MariaDB | partly — unsafe changes are blocked by default, and it has linter checks | yes — changes are verified in a workspace on a real server | Community free; Premium from $124 per month |
| Bytebase | a database governance platform with 200+ SQL review rules | partly; rules that need row counts query the database | yes — it is a deployed platform connected to your databases | Community free up to 20 users and 10 instances; Pro $20 per user per month |
| Squawk | the linter this whole category is measured against | — | no | free and open source |
| Presift | four MySQL rules applied to the .sql files in the pull request | yes, from the statement text only | no — no database, no platform, no account | 30-day evaluation, then paid — below |
Squawk is PostgreSQL. If you are on Postgres, use it: it is free, open source and well established, and Presift has nothing to offer you. The gap this page is about is MySQL-specific.
What it costs, stated plainly
Presift is not free software. Inside GitHub Actions a 30-day evaluation starts by itself when the job has
permissions: id-token: write — no key to request, no form, no account, no card — and it runs the complete product.
After that, continued use needs an organisation licence, intended at $290 per organisation per year. That is an
initial pricing hypothesis, subject to validation: there is no checkout yet, no payment processor connected and
no customer. If that is a dealbreaker, the honest answer is to use Atlas — it does more, and its rules are published.
Trying it takes one workflow
name: migration-safety
on: [pull_request]
permissions:
contents: read
id-token: write # lets the 30-day evaluation start
jobs:
presift:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: nishu-sde/presift@v1
with:
paths: db/migrations
fail-on: error
Linux x86_64 runners with Python 3.9+ (GitHub-hosted ubuntu-* qualify). Reads plain SQL, Flyway,
Liquibase formatted SQL, and the SQL generated by Prisma Migrate, Drizzle Kit, goose, dbmate and golang-migrate. Fork
pull requests cannot start an evaluation, because GitHub issues them no identity token, and outside GitHub Actions the
evaluation is not available.
How the evaluation works The client is open source — read it CI setup and options
If you take nothing else from this page
Look up every ALTER TABLE in your next migration in the
online DDL operations table and read
three columns: in place, rebuilds table, permits concurrent DML. Append , ALGORITHM=INPLACE, LOCK=NONE to the ones
you expect to be online, so MySQL fails fast instead of quietly choosing a copy. That costs nothing and prevents most of this.
A tool is only worth adding if it does it for you on every pull request, forever, without anyone having to remember.