DEV Community

Umadhar M
Umadhar M

Posted on • Originally published at thedevtools.online

Database Migrations & Seeders: Stop Writing Manual SQL Scripts

If you've ever said "I forgot to run the ALTER TABLE on production" — this post is for you.

Manual database changes are one of the most common causes of production incidents. A developer adds a column locally, merges the feature, deploys — and the app crashes because nobody ran the SQL on the live server.

Migrations and seeders solve this completely. But the tooling is different for every stack, and most guides only cover one framework. This one covers all of them.

The Core Idea

Instead of running SQL manually, you write versioned migration files as part of your normal code. They get reviewed in pull requests, committed to git, and run automatically during deployment. Nobody forgets a column again.

Two concepts to understand:

  • Migration — a versioned script for schema changes (CREATE TABLE, ALTER, ADD COLUMN). Applied once, tracked by the framework.
  • Seeder — scripted insertion of reference or initial data. Always written to be idempotent — meaning it can run multiple times without duplicating data.

Why Not Just Write the SQL Directly?

Many teams start with raw SQL scripts shared over Slack or stored in a scripts/ folder. This breaks down fast:

  • No guarantee the script ran on every environment
  • No rollback if something goes wrong
  • No audit trail of who changed what and when
  • New developers have no idea what order to run things in

Migration tools solve all of this by tracking every change in a dedicated table in your database. If a migration has already run, it skips it. If it hasn't, it applies it. Simple and reliable.

Quick Example — Python Flask (Alembic + SQLAlchemy)

# Install
pip install alembic sqlalchemy
alembic init migrations
Enter fullscreen mode Exit fullscreen mode
# migrations/versions/xxxx_add_email_to_users.py
def upgrade():
    op.add_column('users',
        sa.Column('email', sa.String(255), nullable=True)
    )

def downgrade():
    op.drop_column('users', 'email')
Enter fullscreen mode Exit fullscreen mode
# Run it
alembic upgrade head

# Check status
alembic current
alembic history
Enter fullscreen mode Exit fullscreen mode

Seeder — always check before inserting to keep it idempotent:

# seed.py
session = Session()
for role_name in ['ADMIN', 'USER']:
    if not session.query(Role).filter_by(name=role_name).first():
        session.add(Role(name=role_name))
session.commit()
print('Seeding complete.')
Enter fullscreen mode Exit fullscreen mode
python seed.py
Enter fullscreen mode Exit fullscreen mode

What About Existing Apps With No Migrations Yet?

This is the most common question. If your app is already running:

  1. Export your current schema as a baseline migration file
  2. Tell the migration tool to mark it as "already applied"
  3. From that point — all future changes go through migration files only
  4. Update your CI/CD pipeline to run migrations on every deploy

For Flask/Alembic specifically, use alembic stamp head to mark the current state as the baseline without running anything. From that point forward, every schema change is a new migration file.

The Full Guide

The complete article covers all 7 frameworks with full code examples:

  • Spring Boot — Flyway + Hibernate
  • NodeJS — Sequelize CLI
  • Python/Django — Built-in migrations
  • Python/Flask & FastAPI — Alembic + SQLAlchemy
  • Laravel — Artisan migrations
  • Core PHP — Phinx
  • Flutter — sqflite local DB versioning

It also includes:

  • CI/CD pipeline integration examples
  • Post-deploy verification scripts for each stack
  • Idempotency patterns for seeders (MySQL, PostgreSQL, SQLite)
  • A complete FAQ on common migration mistakes

👉 Read the full guide here: Database Migrations & Seeders Best Practices — The DevTools Online


I also built a free collection of developer utilities at thedevtools.online — tools like SQL Formatter, JSON Formatter, JWT Decoder and 40+ more, all browser-based with no login required.

Top comments (0)