Migration should work now

This commit is contained in:
Jonas Linter
2025-10-16 16:16:36 +02:00
parent 48113f6592
commit c43782c664
4 changed files with 161 additions and 1 deletions

View File

@@ -43,6 +43,7 @@ from .db import Reservation as DBReservation
from .email_monitoring import ReservationStatsCollector
from .email_service import create_email_service
from .logging_config import get_logger, setup_logging
from .migrations import run_all_migrations
from .notification_adapters import EmailNotificationAdapter, PushoverNotificationAdapter
from .notification_service import NotificationService
from .pushover_service import create_pushover_service
@@ -276,7 +277,13 @@ async def lifespan(app: FastAPI):
elif hotel_id and not push_endpoint:
_LOGGER.info("Hotel %s has no push_endpoint configured", hotel_id)
# Create tables
# Run database migrations first (only primary worker to avoid race conditions)
if is_primary:
await run_all_migrations(engine)
else:
_LOGGER.info("Skipping migrations (non-primary worker)")
# Create tables (all workers)
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
_LOGGER.info("Database tables checked/created at startup.")

View File

@@ -0,0 +1,115 @@
"""Database migrations for AlpineBits.
This module contains migration functions that are automatically run at app startup
to update existing database schemas without losing data.
"""
from sqlalchemy import inspect, text
from sqlalchemy.ext.asyncio import AsyncEngine
from .logging_config import get_logger
_LOGGER = get_logger(__name__)
async def check_column_exists(engine: AsyncEngine, table_name: str, column_name: str) -> bool:
"""Check if a column exists in a table.
Args:
engine: SQLAlchemy async engine
table_name: Name of the table to check
column_name: Name of the column to check
Returns:
True if column exists, False otherwise
"""
async with engine.connect() as conn:
def _check(connection):
inspector = inspect(connection)
columns = [col['name'] for col in inspector.get_columns(table_name)]
return column_name in columns
result = await conn.run_sync(_check)
return result
async def add_column_if_not_exists(
engine: AsyncEngine,
table_name: str,
column_name: str,
column_type: str = "VARCHAR"
) -> bool:
"""Add a column to a table if it doesn't already exist.
Args:
engine: SQLAlchemy async engine
table_name: Name of the table
column_name: Name of the column to add
column_type: SQL type of the column (default: VARCHAR)
Returns:
True if column was added, False if it already existed
"""
exists = await check_column_exists(engine, table_name, column_name)
if exists:
_LOGGER.debug("Column %s.%s already exists, skipping", table_name, column_name)
return False
_LOGGER.info("Adding column %s.%s (%s)", table_name, column_name, column_type)
async with engine.begin() as conn:
sql = f"ALTER TABLE {table_name} ADD COLUMN {column_name} {column_type}"
await conn.execute(text(sql))
_LOGGER.info("Successfully added column %s.%s", table_name, column_name)
return True
async def migrate_add_room_types(engine: AsyncEngine) -> None:
"""Migration: Add RoomTypes fields to reservations table.
This migration adds three optional fields:
- room_type_code: String (max 8 chars)
- room_classification_code: String (numeric pattern)
- room_type: String (enum: 1-5)
Safe to run multiple times - will skip if columns already exist.
"""
_LOGGER.info("Running migration: add_room_types")
added_count = 0
# Add each column if it doesn't exist
if await add_column_if_not_exists(engine, "reservations", "room_type_code", "VARCHAR"):
added_count += 1
if await add_column_if_not_exists(engine, "reservations", "room_classification_code", "VARCHAR"):
added_count += 1
if await add_column_if_not_exists(engine, "reservations", "room_type", "VARCHAR"):
added_count += 1
if added_count > 0:
_LOGGER.info("Migration add_room_types: Added %d columns", added_count)
else:
_LOGGER.info("Migration add_room_types: No changes needed (already applied)")
async def run_all_migrations(engine: AsyncEngine) -> None:
"""Run all pending migrations.
This function should be called at app startup, before Base.metadata.create_all.
Each migration function should be idempotent (safe to run multiple times).
"""
_LOGGER.info("Starting database migrations...")
try:
# Add new migrations here in chronological order
await migrate_add_room_types(engine)
_LOGGER.info("Database migrations completed successfully")
except Exception as e:
_LOGGER.exception("Migration failed: %s", e)
raise