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()
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()
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:
ProductDailySales
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)
The important addition was:
migrate = Migrate(app, db)
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
Step 2: Initialize the Migration Environment
From the project root in my local development environment, I ran:
flask db init
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
The following structure was created:
migrations/
├── README
├── alembic.ini
├── env.py
├── script.py.mako
└── versions/
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"
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.
The key message was:
No changes in schema detected.
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
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,
)
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
This creates a clearer separation of responsibilities:
Application startup
→ Start and serve the application
Migration operation
→ Change the database schema
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
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
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
Typical commands will be:
flask db migrate -m "describe the schema change"
and:
flask db upgrade
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
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 TABLEsupport - 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
migrationsdirectory 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 initcreates the migration environment -
flask db migratecompares model metadata with the current database schema -
No changes in schema detectedmeans 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-genaiversion mismatch and pytest verification.Part 3 will cover production deployment and automatic migration execution on Render.
sales_data_app Maintenance Series
- https://qiita.com/tosane932/items/ac18b633c8c87b9807bb
- https://qiita.com/tosane932/items/f0aeed574d39c5fa5093
- https://qiita.com/tosane932/items/e19ed4a2ffe27f53faf0
- https://qiita.com/tosane932/items/c1609f17cddf842f1e7c
- https://qiita.com/tosane932/items/b9b6576c1fda3d3a76d2
Pre-Production Inspection Trilogy
- https://qiita.com/tosane932/items/7a15e942745545a37b6a
- https://qiita.com/tosane932/items/3db0e915439048624a40
- https://qiita.com/tosane932/items/b9f32a1b03b99405ad0a
Top comments (0)