The Problem with Naive Schema Changes
Before diving into tools, you need to understand the most important pattern in database CI/CD: Expand-Contract (also called Parallel Change). Without this pattern, even the best tooling will cause production outages.
The naive approach to renaming a column is a single, direct rename statement. That single statement will break any running application instance that still references the old column name email. In a rolling deployment where old and new pods coexist, this causes immediate errors.
The Three-Phase Safe Migration
The Expand-Contract pattern splits a breaking change into three separate deployments:
Phase 1 — Expand: Add the new column (email_address) alongside the old, nullable and with no constraints yet, so both old and new application code can work. Deploy application code that writes to both email and email_address.
Phase 2 — Migrate: Backfill email_address from the existing email values for every row where it's still empty, then — once all rows are populated — add the NOT NULL constraint. Deploy application code that reads from email_address and writes to both.
Phase 3 — Contract: Once all application instances use the new column, drop the old email column. Deploy application code that only references email_address.
This pattern means no single deployment is a breaking change. Each step is independently safe to roll back at the application layer.
A common interview question is: "How would you rename a column in a production database with zero downtime?" The wrong answer is ALTER TABLE users RENAME COLUMN. The correct answer describes the Expand-Contract pattern across three separate deployments, explaining why each phase is necessary and what would break if you skipped directly to the final step.