DEV Community

Cover image for 🧪 Moving Beyond `db.create_all()`: Managing Database Schema History with Flask-Migrate [2/3]
tosane932
tosane932

Posted on

🧪 Moving Beyond `db.create_all()`: Managing Database Schema History with Flask-Migrate [2/3]

Updated on July 14, 2026: Revised Section 2, “Implementation Steps and Observed Results.”

Introduction

Hello from Japan! 🇯🇵

I am tosane932, a professional truck driver working in logistics while teaching myself Python.

In the previous article—the first delivery in this three-part series—I fixed a google-genai version mismatch and verified the existing behavior with pytest.

This time, I moved on to the main task:

Modernizing the way my application manages its database schema.

Until now, I had relied on the convenient command:

db.create_all()
Enter fullscreen mode Exit fullscreen mode

However, I decided to remove that approach and switch to Flask-Migrate, which uses Alembic underneath.

This article records:

  • Why I stopped relying on db.create_all()
  • How I introduced Flask-Migrate
  • How I initialized the migration environment
  • What happened when Alembic compared my models with the existing database
  • How I removed the old automatic table-creation logic

The complete project is available here:

https://github.com/tosane932/sales_data_app


Overview

Previously, my Flask application created database tables using:

db.create_all()
Enter fullscreen mode Exit fullscreen mode

I replaced that approach with schema migration management using:

  • Flask-Migrate
  • Alembic
  • SQLAlchemy
  • Git

The goal was to move from simple table creation toward a system that records and applies database schema changes over time.

This article covers the transition through the initial migration setup.


1. Why I Introduced Flask-Migrate

Until this point, the application executed db.create_all() during startup.

Based on the SQLAlchemy models defined in the project, such as:

  • Product
  • DailySales

the command created any missing tables automatically.

This was convenient during the early stages of development.

However, it had several limitations.

It Does Not Manage Changes to Existing Tables

db.create_all() can create a table that does not yet exist.

However, it does not manage later schema changes such as:

  • Adding a column
  • Removing a column
  • Renaming a column
  • Changing a data type
  • Adding an index
  • Changing a constraint

For example, adding a new field to a SQLAlchemy model does not automatically update an existing production table in a controlled way.

It Does Not Record Schema History

With db.create_all(), there is no migration history showing:

  • What changed
  • When it changed
  • Why it changed
  • Which revision is currently applied
  • How to reproduce the same change in another environment

It Can Create Differences Between Environments

If database changes are handled manually, the following environments may gradually become inconsistent:

  • Local development
  • Docker
  • CI
  • Render PostgreSQL
  • Another developer's environment

The model definitions in the source code may represent one schema while the actual database contains another.

To reduce that risk, I introduced Flask-Migrate so database changes could be recorded as source-controlled migration files.


2. Implementation Steps and Observed Results

Step 1: Install and Configure Flask-Migrate

I installed Flask-Migrate and added it to requirements.txt.

I then integrated it into app.py.

from flask import Flask
from flask_migrate import Migrate

from models import DailySales, Product, db

import config


app = Flask(__name__)

app.config["SQLALCHEMY_DATABASE_URI"] = (
    config.SQLALCHEMY_DATABASE_URI
)

app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = (
    config.SQLALCHEMY_TRACK_MODIFICATIONS
)

db.init_app(app)

migrate = Migrate(app, db)
Enter fullscreen mode Exit fullscreen mode

The important addition was:

migrate = Migrate(app, db)
Enter fullscreen mode Exit fullscreen mode

This connects:

  • The Flask application
  • The SQLAlchemy database object
  • Flask-Migrate
  • Alembic

After this configuration, Flask's CLI can access migration commands such as:

flask db init
flask db migrate
flask db upgrade
Enter fullscreen mode Exit fullscreen mode

Step 2: Initialize the Migration Environment

From the project root in my local development environment, I ran:

flask db init
Enter fullscreen mode Exit fullscreen mode

This generated the migration environment and configuration files.

Actual Output

Creating directory /home/tosane/sales_data_app/migrations ...  done
Creating directory /home/tosane/sales_data_app/migrations/versions ...  done
Generating /home/tosane/sales_data_app/migrations/env.py ...  done
Generating /home/tosane/sales_data_app/migrations/README ...  done
Generating /home/tosane/sales_data_app/migrations/alembic.ini ...  done
Generating /home/tosane/sales_data_app/migrations/script.py.mako ...  done
Enter fullscreen mode Exit fullscreen mode

The following structure was created:

migrations/
├── README
├── alembic.ini
├── env.py
├── script.py.mako
└── versions/
Enter fullscreen mode Exit fullscreen mode

In logistics terms, this directory became the database's maintenance record system.

It provides a place to store future schema-change instructions.


Step 3: Compare the Models with the Existing Database

Next, I attempted to generate the initial migration.

flask db migrate -m "initial migration"
Enter fullscreen mode Exit fullscreen mode

Alembic compared:

  • The current SQLAlchemy model definitions in models.py
  • The existing tables in the local SQLite database

Actual Output

INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.plugins] setting up autogenerate plugin...
INFO  [alembic.env] No changes in schema detected.
Enter fullscreen mode Exit fullscreen mode

The key message was:

No changes in schema detected.
Enter fullscreen mode Exit fullscreen mode

This meant that Alembic did not find a schema difference between the current SQLAlchemy models and the existing local database.

In other words, the model definitions and the database structure were already aligned at that moment.

Because no difference was detected, there was no schema change for Alembic to record in a new migration revision.

This was the expected result for the existing local environment.


What No Changes in Schema Detected Actually Means

This message does not mean that Flask-Migrate failed.

It means:

Model metadata
=
Current database schema
Enter fullscreen mode Exit fullscreen mode

at the time of comparison.

Alembic's autogeneration process looks for differences such as:

  • A model column that does not exist in the database
  • A database table that no longer exists in the models
  • A changed nullable setting
  • A new index
  • A changed data type

If no relevant difference exists, there is nothing to generate.

The migration environment was initialized successfully, but the current database did not require a schema-changing migration.


Step 4: Remove the Old db.create_all() Logic

After introducing Flask-Migrate, I removed the automatic table-creation code from app.py.

The deleted code was:

with app.app_context():
    try:
        db.create_all()

        logger.info(
            "Database tables initialized successfully."
        )

    except Exception as exc:
        logger.error(
            "Failed to initialize database tables: %s",
            exc,
        )
Enter fullscreen mode Exit fullscreen mode

Previously, this block attempted to create missing tables every time the application started.

After the migration system was introduced, schema changes were no longer supposed to happen implicitly during application startup.

Instead, they would be handled explicitly through migration commands.

flask db migrate
flask db upgrade
Enter fullscreen mode Exit fullscreen mode

This creates a clearer separation of responsibilities:

Application startup
→ Start and serve the application
Enter fullscreen mode Exit fullscreen mode
Migration operation
→ Change the database schema
Enter fullscreen mode Exit fullscreen mode

That separation makes database changes more visible and intentional.


Why I Removed db.create_all() Instead of Keeping Both

It would have been possible to keep db.create_all() while also introducing Flask-Migrate.

However, that would create two separate systems attempting to manage the database schema.

db.create_all()
+
Flask-Migrate
Enter fullscreen mode Exit fullscreen mode

That could make it difficult to determine:

  • Which process created a table
  • Whether a migration had actually been applied
  • Why one environment differed from another
  • Whether startup behavior was hiding a missing migration

I wanted one clear source of truth for schema changes.

For that reason, I removed the old startup-based creation logic after introducing the migration framework.


The New Database Management Flow

Before the change, the process was roughly:

Start the Flask application
        ↓
Run db.create_all()
        ↓
Create missing tables
        ↓
Continue startup
Enter fullscreen mode Exit fullscreen mode

After the change, the intended process became:

Change a SQLAlchemy model
        ↓
Generate a migration revision
        ↓
Review the generated migration file
        ↓
Commit it to Git
        ↓
Apply the migration
        ↓
Start the application using the updated schema
Enter fullscreen mode Exit fullscreen mode

Typical commands will be:

flask db migrate -m "describe the schema change"
Enter fullscreen mode Exit fullscreen mode

and:

flask db upgrade
Enter fullscreen mode Exit fullscreen mode

The generated migration files can then be reviewed and stored in Git together with the application code.


Why Migration Files Must Be Reviewed

Flask-Migrate and Alembic can detect many schema differences automatically.

However, automatically generated migration files should not be treated as unquestionable output.

Before applying a migration, I should confirm:

  • Which tables will be changed
  • Which columns will be added or removed
  • Whether any data could be lost
  • Whether SQLite and PostgreSQL behave differently
  • Whether the downgrade operation is valid
  • Whether existing production data needs transformation

The command:

flask db migrate
Enter fullscreen mode Exit fullscreen mode

generates a proposal based on detected differences.

It does not replace human review.

That is similar to a pre-departure inspection sheet.

The existence of the document is useful, but someone still needs to inspect what it says before departure.


SQLite and PostgreSQL Require Extra Attention

My local environment and production environment do not necessarily use the same database engine.

The project has used:

  • SQLite locally
  • PostgreSQL in production

A migration that behaves correctly in SQLite may not behave exactly the same way in PostgreSQL.

Possible differences include:

  • Supported data types
  • Constraint behavior
  • ALTER TABLE support
  • Default values
  • Index behavior
  • Transaction handling

For that reason, the migration process must eventually be verified against the production-style PostgreSQL environment as well.

The local migration setup is only the foundation.

It does not automatically guarantee that every future migration will be safe in production.


Current Result

After this work, the application reached the following state:

  • Flask-Migrate was installed
  • The dependency was added to requirements.txt
  • Flask-Migrate was connected to the Flask application
  • The migrations directory was initialized
  • Alembic compared the current models with the existing SQLite schema
  • No schema differences were detected
  • The old db.create_all() startup logic was removed
  • Future schema changes can now be represented as migration files

The database-management process is now more explicit and easier to reproduce.


Summary

This work replaced startup-time table creation with a migration-based database management foundation.

The main lessons were:

  • db.create_all() is useful for creating missing tables, but it does not manage schema history
  • Existing table changes require a migration system
  • Flask-Migrate connects Flask and SQLAlchemy to Alembic
  • flask db init creates the migration environment
  • flask db migrate compares model metadata with the current database schema
  • No changes in schema detected means no relevant schema difference was found
  • Migration generation and migration application are separate operations
  • Automatically generated migration files still require human review
  • Running both db.create_all() and migrations can create unclear ownership of the schema
  • Local SQLite verification does not automatically prove PostgreSQL compatibility

By removing db.create_all() and introducing Flask-Migrate, I established the foundation for managing database changes through Git.

The next step is:

Applying migrations safely in the Render PostgreSQL production environment.


This is Part 2 of a three-part series.

Part 1 covered the google-genai version mismatch and pytest verification.

Part 3 will cover production deployment and automatic migration execution on Render.

sales_data_app Maintenance Series

Pre-Production Inspection Trilogy

Post-Deployment Maintenance Series

🍞 sales_data_app on GitHub

Top comments (0)