175 lines
6.1 KiB
Markdown
175 lines
6.1 KiB
Markdown
# Database Migration Refactoring
|
|
|
|
## Summary
|
|
|
|
This refactoring changes the database handling from manual schema migrations in `migrations.py` to using Alembic for proper database migrations. The key improvements are:
|
|
|
|
1. **Alembic Integration**: All schema migrations now use Alembic's migration framework
|
|
2. **Separation of Concerns**: Migrations (schema changes) are separated from startup tasks (data backfills)
|
|
3. **Pre-startup Migrations**: Database migrations run BEFORE the application starts, avoiding issues with multiple workers
|
|
4. **Production Ready**: The Conversions/ConversionRoom tables can be safely recreated (data is recoverable from PMS XML imports)
|
|
|
|
## Changes Made
|
|
|
|
### 1. Alembic Setup
|
|
|
|
- **[alembic.ini](alembic.ini)**: Configuration file for Alembic
|
|
- **[alembic/env.py](alembic/env.py)**: Async-compatible environment setup that:
|
|
- Loads database URL from config.yaml or environment variables
|
|
- Supports PostgreSQL schemas
|
|
- Uses async SQLAlchemy engine
|
|
|
|
### 2. Initial Migrations
|
|
|
|
Two migrations were created:
|
|
|
|
#### Migration 1: `535b70e85b64_initial_schema.py`
|
|
Creates all base tables:
|
|
- `customers`
|
|
- `hashed_customers`
|
|
- `reservations`
|
|
- `acked_requests`
|
|
- `conversions`
|
|
- `conversion_rooms`
|
|
|
|
This migration is idempotent - it only creates missing tables.
|
|
|
|
#### Migration 2: `8edfc81558db_drop_and_recreate_conversions_tables.py`
|
|
Handles the conversion from old production conversions schema to new normalized schema:
|
|
- Detects if old conversions tables exist with incompatible schema
|
|
- Drops them if needed (data can be recreated from PMS XML imports)
|
|
- Allows the initial schema migration to recreate them with correct structure
|
|
|
|
### 3. Refactored Files
|
|
|
|
#### [src/alpine_bits_python/db_setup.py](src/alpine_bits_python/db_setup.py)
|
|
- **Before**: Ran manual migrations AND created tables using Base.metadata.create_all
|
|
- **After**: Only runs startup tasks (data backfills like customer hashing)
|
|
- **Note**: Schema migrations now handled by Alembic
|
|
|
|
#### [src/alpine_bits_python/run_migrations.py](src/alpine_bits_python/run_migrations.py) (NEW)
|
|
- Wrapper script to run `alembic upgrade head`
|
|
- Can be called standalone or from run_api.py
|
|
- Handles errors gracefully
|
|
|
|
#### [src/alpine_bits_python/api.py](src/alpine_bits_python/api.py)
|
|
- **Removed**: `run_all_migrations()` call from lifespan
|
|
- **Removed**: `Base.metadata.create_all()` call
|
|
- **Changed**: Now only calls `run_startup_tasks()` for data backfills
|
|
- **Note**: Assumes migrations have already been run before app start
|
|
|
|
#### [src/alpine_bits_python/run_api.py](src/alpine_bits_python/run_api.py)
|
|
- **Added**: Calls `run_migrations()` BEFORE starting uvicorn
|
|
- **Benefit**: Migrations complete before any worker starts
|
|
- **Benefit**: Works correctly with multiple workers
|
|
|
|
### 4. Old Files (Can be removed in future cleanup)
|
|
|
|
- **[src/alpine_bits_python/migrations.py](src/alpine_bits_python/migrations.py)**: Old manual migration functions
|
|
- These can be safely removed once you verify the Alembic setup works
|
|
- The functionality has been replaced by Alembic migrations
|
|
|
|
## Usage
|
|
|
|
### Development
|
|
|
|
Start the server (migrations run automatically):
|
|
```bash
|
|
uv run python -m alpine_bits_python.run_api
|
|
```
|
|
|
|
Or run migrations separately:
|
|
```bash
|
|
uv run alembic upgrade head
|
|
uv run python -m alpine_bits_python.run_api
|
|
```
|
|
|
|
### Production with Multiple Workers
|
|
|
|
The migrations automatically run before uvicorn starts, so you can safely use:
|
|
```bash
|
|
# Migrations run once, then server starts with multiple workers
|
|
uv run python -m alpine_bits_python.run_api
|
|
|
|
# Or with uvicorn directly (migrations won't run automatically):
|
|
uv run alembic upgrade head # Run this first
|
|
uvicorn alpine_bits_python.api:app --workers 4 --host 0.0.0.0 --port 8080
|
|
```
|
|
|
|
### Creating New Migrations
|
|
|
|
When you modify the database schema in `db.py`:
|
|
|
|
```bash
|
|
# Generate migration automatically
|
|
uv run alembic revision --autogenerate -m "description_of_change"
|
|
|
|
# Or create empty migration to fill in manually
|
|
uv run alembic revision -m "description_of_change"
|
|
|
|
# Review the generated migration in alembic/versions/
|
|
# Then apply it
|
|
uv run alembic upgrade head
|
|
```
|
|
|
|
### Checking Migration Status
|
|
|
|
```bash
|
|
# Show current revision
|
|
uv run alembic current
|
|
|
|
# Show migration history
|
|
uv run alembic history
|
|
|
|
# Show pending migrations
|
|
uv run alembic heads
|
|
```
|
|
|
|
## Benefits
|
|
|
|
1. **Multiple Worker Safe**: Migrations run once before any worker starts
|
|
2. **Proper Migration History**: All schema changes are tracked in version control
|
|
3. **Rollback Support**: Can downgrade to previous schema versions if needed
|
|
4. **Standard Tool**: Alembic is the industry-standard migration tool for SQLAlchemy
|
|
5. **Separation of Concerns**:
|
|
- Schema migrations (Alembic) are separate from startup tasks (db_setup.py)
|
|
- Migrations are separate from application code
|
|
|
|
## Migration from Old System
|
|
|
|
If you have an existing database with the old migration system:
|
|
|
|
1. The initial migration will detect existing tables and skip creating them
|
|
2. The conversions table migration will detect old schemas and recreate them
|
|
3. All data in other tables is preserved
|
|
4. Conversions data will be lost but can be recreated from PMS XML imports
|
|
|
|
## Important Notes
|
|
|
|
### Conversions Table Data Loss
|
|
|
|
The `conversions` and `conversion_rooms` tables will be dropped and recreated with the new schema. This is intentional because:
|
|
- The production version has a different schema
|
|
- The data can be recreated by re-importing PMS XML files
|
|
- This avoids complex data migration logic
|
|
|
|
If you need to preserve this data, modify the migration before running it.
|
|
|
|
### Future Migrations
|
|
|
|
In the future, when you need to change the database schema:
|
|
|
|
1. Modify the model classes in `db.py`
|
|
2. Generate an Alembic migration: `uv run alembic revision --autogenerate -m "description"`
|
|
3. Review the generated migration carefully
|
|
4. Test it on a dev database first
|
|
5. Apply it to production: `uv run alembic upgrade head`
|
|
|
|
## Configuration
|
|
|
|
The Alembic setup reads configuration from the same sources as the application:
|
|
- `config.yaml` (via `annotatedyaml` with `secrets.yaml`)
|
|
- Environment variables (`DATABASE_URL`, `DATABASE_SCHEMA`)
|
|
|
|
No additional configuration needed!
|