mirror of
https://github.com/pablorevilla-meshtastic/meshview.git
synced 2026-03-04 23:27:46 +01:00
361 lines
10 KiB
Markdown
361 lines
10 KiB
Markdown
# 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
|
||
|
||
1. **`meshview/migrations.py`** - Migration management utilities
|
||
- `run_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
|
||
|
||
2. **`alembic/`** - Alembic migration directory
|
||
- `env.py` - Configured for async SQLAlchemy support
|
||
- `versions/` - Migration scripts directory
|
||
- `alembic.ini` - Alembic configuration
|
||
|
||
3. **Modified Apps**:
|
||
- `startdb.py` - Writer app that runs migrations before MQTT ingestion
|
||
- `meshview/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:
|
||
1. Deploy new code with migration files
|
||
2. Restart the applications
|
||
|
||
### Writer App (startdb.py) Startup Sequence
|
||
|
||
1. Initialize database connection
|
||
2. Create migration status tracking table
|
||
3. Set "migration in progress" flag
|
||
4. **🔄 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
|
||
5. Clear "migration in progress" flag
|
||
6. Start MQTT ingestion and other tasks
|
||
|
||
### Reader App (web.py) Startup Sequence
|
||
|
||
1. Initialize database connection
|
||
2. **Check database schema version**
|
||
3. 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
|
||
4. Once schema is current, start web server
|
||
|
||
### 🎯 Key Point: Zero Manual Steps
|
||
|
||
When you deploy new code with migrations:
|
||
```bash
|
||
# 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:
|
||
|
||
```bash
|
||
./env/bin/python create_migration.py
|
||
```
|
||
|
||
### Manual creation:
|
||
|
||
```bash
|
||
./env/bin/alembic revision --autogenerate -m "Description of changes"
|
||
```
|
||
|
||
This will:
|
||
1. Compare current database schema with SQLAlchemy models
|
||
2. Generate a migration script in `alembic/versions/`
|
||
3. Automatically detect most schema changes
|
||
|
||
### Manual migration (advanced):
|
||
|
||
```bash
|
||
./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:
|
||
|
||
```bash
|
||
./env/bin/python startdb.py
|
||
```
|
||
|
||
### Manual
|
||
|
||
To run migrations manually:
|
||
|
||
```bash
|
||
./env/bin/alembic upgrade head
|
||
```
|
||
|
||
To downgrade:
|
||
|
||
```bash
|
||
./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:
|
||
|
||
```bash
|
||
./env/bin/alembic current
|
||
```
|
||
|
||
View migration history:
|
||
|
||
```bash
|
||
./env/bin/alembic history
|
||
```
|
||
|
||
## Benefits
|
||
|
||
1. **Zero Manual Intervention**: Migrations run automatically on startup
|
||
2. **Safe Coordination**: Reader won't connect to incompatible schema
|
||
3. **Version Control**: All schema changes tracked in git
|
||
4. **Rollback Capability**: Can downgrade if needed
|
||
5. **Auto-generation**: Most migrations created automatically from model changes
|
||
|
||
## Migration Workflow
|
||
|
||
### Development Process
|
||
|
||
1. **Modify SQLAlchemy models** in `meshview/models.py`
|
||
2. **Create migration**:
|
||
```bash
|
||
./env/bin/python create_migration.py
|
||
```
|
||
3. **Review generated migration** in `alembic/versions/`
|
||
4. **Test migration**:
|
||
- Stop all apps
|
||
- Start writer app (migrations run automatically)
|
||
- Start reader app (waits for schema to be current)
|
||
5. **Commit migration** to version control
|
||
|
||
### Production Deployment
|
||
|
||
1. **Deploy new code** with migration scripts
|
||
2. **Start writer app** - Migrations run automatically
|
||
3. **Start reader app** - Waits for migrations, then starts
|
||
4. **Monitor logs** for migration success
|
||
|
||
## Troubleshooting
|
||
|
||
### Migration fails
|
||
|
||
Check logs in writer app for error details. To manually fix:
|
||
|
||
```bash
|
||
./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:
|
||
|
||
```bash
|
||
./env/bin/alembic current
|
||
```
|
||
|
||
### Reset to clean state
|
||
|
||
⚠️ **Warning: This will lose all data**
|
||
|
||
```bash
|
||
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`:
|
||
|
||
```ini
|
||
[database]
|
||
connection_string = sqlite+aiosqlite:///packets.db
|
||
```
|
||
|
||
Alembic automatically uses this configuration through `meshview/migrations.py`.
|
||
|
||
## Important Notes
|
||
|
||
1. **Always test migrations** in development before deploying to production
|
||
2. **Backup database** before running migrations in production
|
||
3. **Check for data loss** - Some migrations may require data migration logic
|
||
4. **Coordinate deployments** - Start writer before readers in multi-instance setups
|
||
5. **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):
|
||
|
||
```python
|
||
"""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:
|
||
|
||
```python
|
||
"""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:**
|
||
|
||
```bash
|
||
# 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):
|
||
|
||
```bash
|
||
# 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
|
||
```
|
||
|
||
## References
|
||
|
||
- [Alembic Documentation](https://alembic.sqlalchemy.org/)
|
||
- [SQLAlchemy Documentation](https://docs.sqlalchemy.org/)
|
||
- [Async SQLAlchemy](https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html) |