From c43782c6646ada46d52704d73b226a547b2dc6fa Mon Sep 17 00:00:00 2001 From: Jonas Linter Date: Thu, 16 Oct 2025 16:16:36 +0200 Subject: [PATCH] Migration should work now --- alpinebits.log | 38 +++++++++ src/alpine_bits_python/api.py | 9 ++- src/alpine_bits_python/migrations.py | 115 +++++++++++++++++++++++++++ test_migration.db | Bin 0 -> 16384 bytes 4 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 src/alpine_bits_python/migrations.py create mode 100644 test_migration.db diff --git a/alpinebits.log b/alpinebits.log index 94a6c66..46220ea 100644 --- a/alpinebits.log +++ b/alpinebits.log @@ -14073,3 +14073,41 @@ IndexError: list index out of range 2025-10-15 08:52:56 - root - INFO - Logging to file: alpinebits.log 2025-10-15 08:52:56 - root - INFO - Logging configured at INFO level 2025-10-15 08:52:58 - alpine_bits_python.email_service - INFO - Email service initialized: smtp.titan.email:465 +2025-10-16 16:15:42 - root - INFO - Logging to file: alpinebits.log +2025-10-16 16:15:42 - root - INFO - Logging configured at INFO level +2025-10-16 16:15:42 - alpine_bits_python.email_monitoring - INFO - DailyReportScheduler initialized: send_time=08:00, recipients=[] +2025-10-16 16:15:42 - root - INFO - Daily report scheduler configured for Pushover (primary worker) +2025-10-16 16:15:42 - alpine_bits_python.api - INFO - Application startup initiated (primary_worker=True) +2025-10-16 16:15:42 - alpine_bits_python.alpinebits_server - INFO - Initializing action instance for AlpineBitsActionName.OTA_HOTEL_NOTIF_REPORT +2025-10-16 16:15:42 - alpine_bits_python.alpinebits_server - INFO - Initializing action instance for AlpineBitsActionName.OTA_PING +2025-10-16 16:15:42 - alpine_bits_python.alpinebits_server - INFO - Initializing action instance for AlpineBitsActionName.OTA_HOTEL_RES_NOTIF_GUEST_REQUESTS +2025-10-16 16:15:42 - alpine_bits_python.alpinebits_server - INFO - Initializing action instance for AlpineBitsActionName.OTA_READ +2025-10-16 16:15:42 - alpine_bits_python.api - INFO - Hotel 39054_001 has no push_endpoint configured +2025-10-16 16:15:42 - alpine_bits_python.api - INFO - Hotel 135 has no push_endpoint configured +2025-10-16 16:15:42 - alpine_bits_python.api - INFO - Hotel 39052_001 has no push_endpoint configured +2025-10-16 16:15:42 - alpine_bits_python.api - INFO - Hotel 39040_001 has no push_endpoint configured +2025-10-16 16:15:42 - alpine_bits_python.migrations - INFO - Starting database migrations... +2025-10-16 16:15:42 - alpine_bits_python.migrations - INFO - Running migration: add_room_types +2025-10-16 16:15:42 - alpine_bits_python.migrations - INFO - Adding column reservations.room_type_code (VARCHAR) +2025-10-16 16:15:42 - alpine_bits_python.migrations - INFO - Successfully added column reservations.room_type_code +2025-10-16 16:15:42 - alpine_bits_python.migrations - INFO - Adding column reservations.room_classification_code (VARCHAR) +2025-10-16 16:15:42 - alpine_bits_python.migrations - INFO - Successfully added column reservations.room_classification_code +2025-10-16 16:15:42 - alpine_bits_python.migrations - INFO - Adding column reservations.room_type (VARCHAR) +2025-10-16 16:15:42 - alpine_bits_python.migrations - INFO - Successfully added column reservations.room_type +2025-10-16 16:15:42 - alpine_bits_python.migrations - INFO - Migration add_room_types: Added 3 columns +2025-10-16 16:15:42 - alpine_bits_python.migrations - INFO - Database migrations completed successfully +2025-10-16 16:15:42 - alpine_bits_python.api - INFO - Database tables checked/created at startup. +2025-10-16 16:15:42 - alpine_bits_python.api - INFO - All existing customers already have hashed data +2025-10-16 16:15:42 - alpine_bits_python.email_monitoring - INFO - ReservationStatsCollector initialized with 4 hotels +2025-10-16 16:15:42 - alpine_bits_python.api - INFO - Stats collector initialized and hooked up to report scheduler +2025-10-16 16:15:42 - alpine_bits_python.api - INFO - Sending test daily report on startup (last 24 hours) +2025-10-16 16:15:42 - alpine_bits_python.email_monitoring - INFO - Collecting reservation stats from 2025-10-15 16:15:42 to 2025-10-16 16:15:42 +2025-10-16 16:15:42 - alpine_bits_python.email_monitoring - INFO - Collected stats: 9 total reservations across 1 hotels +2025-10-16 16:15:42 - alpine_bits_python.email_service - WARNING - No recipients specified for email: AlpineBits Daily Report - 2025-10-16 +2025-10-16 16:15:42 - alpine_bits_python.api - ERROR - Failed to send test daily report via email on startup +2025-10-16 16:15:42 - alpine_bits_python.pushover_service - INFO - Pushover notification sent successfully: AlpineBits Daily Report - 2025-10-16 +2025-10-16 16:15:42 - alpine_bits_python.api - INFO - Test daily report sent via Pushover successfully on startup +2025-10-16 16:15:42 - alpine_bits_python.email_monitoring - INFO - Daily report scheduler started +2025-10-16 16:15:42 - alpine_bits_python.api - INFO - Daily report scheduler started +2025-10-16 16:15:42 - alpine_bits_python.api - INFO - Application startup complete +2025-10-16 16:15:42 - alpine_bits_python.email_monitoring - INFO - Next daily report scheduled for 2025-10-17 08:00:00 (in 15.7 hours) diff --git a/src/alpine_bits_python/api.py b/src/alpine_bits_python/api.py index f210d7d..ffdb6b7 100644 --- a/src/alpine_bits_python/api.py +++ b/src/alpine_bits_python/api.py @@ -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.") diff --git a/src/alpine_bits_python/migrations.py b/src/alpine_bits_python/migrations.py new file mode 100644 index 0000000..68a2363 --- /dev/null +++ b/src/alpine_bits_python/migrations.py @@ -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 diff --git a/test_migration.db b/test_migration.db new file mode 100644 index 0000000000000000000000000000000000000000..512a004a6f93c6ebc46bbfcc44dca13d08cf80d1 GIT binary patch literal 16384 zcmeI#L2KJE6bEp*DO(AI^wz5`ffh<(Euq(Wo!LTDxAmMsPeQen)L_Z0ESG(NF}hE+ zueIakcwU{jg&jxu3v9>lJ%y*A?SnpDPg5)DJU5D4dO~)I=aILR5<)z8Y`dcw&)qk< zm#!W;diXII{vy4>m<-0lpTqAjl_CxS2tWV=5P$##AOHafK;SEIO#|A_(EUDjBNcQK#=(amqTyvs!`XC7ucFC?AKlRN z;O1~yl$3?dl{D7N`AzdoQMVZ;6>9{Vf;>lf_2_yAtrin7XkVVsBJt?2hrCALnM!FsZb9>OL zk9czNrPm z>z^mSX;19AuWG0IvIf;@w>n`!00Izz00bZa0SG_<0uX=z1R$`f0{v=X*#B?pHq)$ literal 0 HcmV?d00001