From db6826cb49d97fd0c7464b3d710b2a2ea3861853 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 2 Aug 2026 12:36:10 +0000 Subject: [PATCH] no-mistakes(review): Add startup self-heal guard for show_in_form column --- app/main.py | 13 +++++++++++ tests/test_categories_and_units.py | 35 ++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/app/main.py b/app/main.py index 9947821..ccdc7d5 100644 --- a/app/main.py +++ b/app/main.py @@ -9,6 +9,7 @@ from pathlib import Path from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles +from sqlalchemy import text from app.core.config import settings from app.core.database import Base, async_session_factory, engine @@ -18,12 +19,24 @@ from app.services.seed import seed_categories, seed_units, seed_users logger = logging.getLogger(__name__) +async def ensure_legacy_schema(conn) -> None: + """Add columns from alembic migrations that legacy create_all databases lack.""" + result = await conn.execute(text("PRAGMA table_info(categories)")) + columns = {row[1] for row in result} + if "show_in_form" not in columns: + await conn.execute( + text("ALTER TABLE categories ADD COLUMN show_in_form BOOLEAN NOT NULL DEFAULT 1") + ) + logger.info("Added missing categories.show_in_form column (legacy database)") + + @asynccontextmanager async def lifespan(app: FastAPI): """Initialise database and seed data on startup.""" logger.info("Starting Denya OneCare …") async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) + await ensure_legacy_schema(conn) async with async_session_factory() as session: await seed_users(session) await session.commit() diff --git a/tests/test_categories_and_units.py b/tests/test_categories_and_units.py index 589fc2e..3956a35 100644 --- a/tests/test_categories_and_units.py +++ b/tests/test_categories_and_units.py @@ -138,6 +138,41 @@ async def test_seed_syncs_show_in_form_on_existing_rows(client): assert len(created) >= 1 +async def test_legacy_db_self_heals_show_in_form_column(): + """A pre-Sprint-A categories table (no show_in_form) gets the column on startup.""" + from sqlalchemy import text + + from app.core.database import engine + from app.main import ensure_legacy_schema + + async with engine.begin() as conn: + await conn.execute(text("DROP TABLE IF EXISTS tickets")) + await conn.execute(text("DROP TABLE IF EXISTS categories")) + await conn.execute( + text( + "CREATE TABLE categories (" + "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " + "type VARCHAR(20) NOT NULL, " + "name VARCHAR(100) NOT NULL, " + "parent_id INTEGER, " + "sla_urgency VARCHAR(10))" + ) + ) + await ensure_legacy_schema(conn) + result = await conn.execute(text("PRAGMA table_info(categories)")) + assert "show_in_form" in {row[1] for row in result} + + # Idempotent on the next startup + async with engine.begin() as conn: + await ensure_legacy_schema(conn) + result = await conn.execute(text("PRAGMA table_info(categories)")) + assert "show_in_form" in {row[1] for row in result} + + async with engine.begin() as conn: + await conn.execute(text("DROP TABLE IF EXISTS categories")) + await conn.execute(text("DROP TABLE IF EXISTS tickets")) + + # ── Unit hierarchy ──────────────────────────────────────────────────── async def test_units_building_filter(client): """GET /api/tickets/units?building= filters to one building."""