10 KiB
Alembic Database Migration Setup
This document describes the automatic database migration system implemented for MeshView using Alembic.
Overview
The system provides automatic database schema migrations with coordination between the writer app (startdb.py) and reader app (web.py):
- Writer App: Automatically runs pending migrations on startup
- Reader App: Waits for migrations to complete before starting
Architecture
Key Components
-
meshview/migrations.py- Migration management utilitiesrun_migrations()- Runs pending migrations (writer app)wait_for_migrations()- Waits for schema to be current (reader app)is_database_up_to_date()- Checks schema version- Migration status tracking table
-
alembic/- Alembic migration directoryenv.py- Configured for async SQLAlchemy supportversions/- Migration scripts directoryalembic.ini- Alembic configuration
-
Modified Apps:
startdb.py- Writer app that runs migrations before MQTT ingestionmeshview/web.py- Reader app that waits for schema updates
How It Works - Automatic In-Place Updates
✨ Fully Automatic Operation
No manual migration commands needed! The database schema updates automatically when you:
- Deploy new code with migration files
- Restart the applications
Writer App (startdb.py) Startup Sequence
- Initialize database connection
- Create migration status tracking table
- Set "migration in progress" flag
- 🔄 Automatically run any pending Alembic migrations (synchronously)
- Detects current schema version
- Compares to latest available migration
- Runs all pending migrations in sequence
- Updates database schema in place
- Clear "migration in progress" flag
- Start MQTT ingestion and other tasks
Reader App (web.py) Startup Sequence
- Initialize database connection
- Check database schema version
- If not up to date:
- Wait up to 60 seconds (30 retries × 2 seconds)
- Check every 2 seconds for schema updates
- Automatically proceeds once writer completes migrations
- Once schema is current, start web server
🎯 Key Point: Zero Manual Steps
When you deploy new code with migrations:
# Just start the apps - migrations happen automatically!
./env/bin/python startdb.py # Migrations run here automatically
./env/bin/python main.py # Waits for migrations, then starts
The database updates itself! No need to run alembic upgrade manually.
Coordination
The apps coordinate using:
- Alembic version table (
alembic_version) - Tracks current schema version - Migration status table (
migration_status) - Optional flag for "in progress" state
Creating New Migrations
Using the helper script:
./env/bin/python create_migration.py
Manual creation:
./env/bin/alembic revision --autogenerate -m "Description of changes"
This will:
- Compare current database schema with SQLAlchemy models
- Generate a migration script in
alembic/versions/ - Automatically detect most schema changes
Manual migration (advanced):
./env/bin/alembic revision -m "Manual migration"
Then edit the generated file to add custom migration logic.
Running Migrations
Automatic (Recommended)
Migrations run automatically when the writer app starts:
./env/bin/python startdb.py
Manual
To run migrations manually:
./env/bin/alembic upgrade head
To downgrade:
./env/bin/alembic downgrade -1 # Go back one version
./env/bin/alembic downgrade base # Go back to beginning
Checking Migration Status
Check current database version:
./env/bin/alembic current
View migration history:
./env/bin/alembic history
Benefits
- Zero Manual Intervention: Migrations run automatically on startup
- Safe Coordination: Reader won't connect to incompatible schema
- Version Control: All schema changes tracked in git
- Rollback Capability: Can downgrade if needed
- Auto-generation: Most migrations created automatically from model changes
Migration Workflow
Development Process
- Modify SQLAlchemy models in
meshview/models.py - Create migration:
./env/bin/python create_migration.py - Review generated migration in
alembic/versions/ - Test migration:
- Stop all apps
- Start writer app (migrations run automatically)
- Start reader app (waits for schema to be current)
- Commit migration to version control
Production Deployment
- Deploy new code with migration scripts
- Start writer app - Migrations run automatically
- Start reader app - Waits for migrations, then starts
- Monitor logs for migration success
Troubleshooting
Migration fails
Check logs in writer app for error details. To manually fix:
./env/bin/alembic current # Check current version
./env/bin/alembic history # View available versions
./env/bin/alembic upgrade head # Try manual upgrade
Reader app won't start (timeout)
Check if writer app is running and has completed migrations:
./env/bin/alembic current
Reset to clean state
⚠️ Warning: This will lose all data
rm packets.db # Or your database file
./env/bin/alembic upgrade head # Create fresh schema
File Structure
meshview/
├── alembic.ini # Alembic configuration
├── alembic/
│ ├── env.py # Async-enabled migration runner
│ ├── script.py.mako # Migration template
│ └── versions/ # Migration scripts
│ └── c88468b7ab0b_initial_migration.py
├── meshview/
│ ├── models.py # SQLAlchemy models (source of truth)
│ ├── migrations.py # Migration utilities
│ ├── mqtt_database.py # Writer database connection
│ └── database.py # Reader database connection
├── startdb.py # Writer app (runs migrations)
├── main.py # Entry point for reader app
└── create_migration.py # Helper script for creating migrations
Configuration
Database URL is read from config.ini:
[database]
connection_string = sqlite+aiosqlite:///packets.db
Alembic automatically uses this configuration through meshview/migrations.py.
Important Notes
- Always test migrations in development before deploying to production
- Backup database before running migrations in production
- Check for data loss - Some migrations may require data migration logic
- Coordinate deployments - Start writer before readers in multi-instance setups
- Monitor logs during first startup after deployment
Example Migrations
Example 1: Generated Initial Migration
Here's what an auto-generated migration looks like (from comparing models to database):
"""Initial migration
Revision ID: c88468b7ab0b
Revises:
Create Date: 2025-01-26 20:56:50.123456
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers
revision = 'c88468b7ab0b'
down_revision = None
branch_labels = None
depends_on = None
def upgrade() -> None:
# Upgrade operations
op.create_table('node',
sa.Column('id', sa.String(), nullable=False),
sa.Column('node_id', sa.BigInteger(), nullable=True),
# ... more columns
sa.PrimaryKeyConstraint('id')
)
def downgrade() -> None:
# Downgrade operations
op.drop_table('node')
Example 2: Manual Migration Adding a New Table
We've included an example migration (1717fa5c6545_add_example_table.py) that demonstrates how to manually create a new table:
"""Add example table
Revision ID: 1717fa5c6545
Revises: c88468b7ab0b
Create Date: 2025-10-26 20:59:04.347066
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
def upgrade() -> None:
"""Create example table with sample columns."""
op.create_table(
'example',
sa.Column('id', sa.Integer(), nullable=False, primary_key=True, autoincrement=True),
sa.Column('name', sa.String(length=100), nullable=False),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('value', sa.Float(), nullable=True),
sa.Column('is_active', sa.Boolean(), nullable=False, server_default='1'),
sa.Column('created_at', sa.DateTime(), nullable=False,
server_default=sa.text('CURRENT_TIMESTAMP')),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
# Create an index on the name column for faster lookups
op.create_index('idx_example_name', 'example', ['name'])
def downgrade() -> None:
"""Remove example table."""
op.drop_index('idx_example_name', table_name='example')
op.drop_table('example')
Key features demonstrated:
- Various column types (Integer, String, Text, Float, Boolean, DateTime)
- Primary key with autoincrement
- Nullable and non-nullable columns
- Server defaults (for timestamps and booleans)
- Creating indexes
- Proper downgrade that reverses all changes
To test this migration:
# Apply the migration
./env/bin/alembic upgrade head
# Check it was applied
./env/bin/alembic current
# Verify table was created
sqlite3 packetsPL.db "SELECT sql FROM sqlite_master WHERE type='table' AND name='example';"
# Roll back the migration
./env/bin/alembic downgrade -1
# Verify table was removed
sqlite3 packetsPL.db "SELECT name FROM sqlite_master WHERE type='table' AND name='example';"
To remove this example migration (after testing):
# First make sure you're not on this revision
./env/bin/alembic downgrade c88468b7ab0b
# Then delete the migration file
rm alembic/versions/1717fa5c6545_add_example_table.py