116 lines
3.6 KiB
Python
116 lines
3.6 KiB
Python
"""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, after 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
|