Skip to content

Commit f4fc681

Browse files
committed
feat(backend): add database schema verification and migration handling
- Implement schema verification on application startup to check for required columns in the generated_roadmaps table. - Log warnings and attempt to run migrations if required columns are missing. - Enhance the list_roadmaps function to differentiate between production and test environments for database calls, improving error handling and logging.
1 parent f5bd188 commit f4fc681

File tree

2 files changed

+104
-31
lines changed

2 files changed

+104
-31
lines changed

commitly-backend/app/api/roadmap.py

Lines changed: 60 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -69,22 +69,34 @@ async def list_roadmaps(
6969
import time
7070

7171
start_time = time.time()
72-
logger.info("list_roadmaps: Calling service.list_catalog via executor")
72+
logger.info("list_roadmaps: Calling service.list_catalog")
7373

74+
# Wrap the sync database call in executor to avoid blocking event loop
75+
# The service.list_catalog is async but calls sync DB code internally
7476
def call_db():
7577
try:
7678
logger.info("list_roadmaps.executor: Starting database call")
77-
return service._result_store.list_catalog(
78-
page=page,
79-
page_size=page_size,
80-
language=language,
81-
tag=tag,
82-
difficulty=difficulty,
83-
min_rating=min_rating,
84-
min_views=min_views,
85-
min_syncs=min_syncs,
86-
sort=sort,
87-
)
79+
# Use the service's internal result_store directly for executor
80+
# This avoids issues with async/sync boundaries
81+
if hasattr(service, "_result_store"):
82+
return service._result_store.list_catalog(
83+
page=page,
84+
page_size=page_size,
85+
language=language,
86+
tag=tag,
87+
difficulty=difficulty,
88+
min_rating=min_rating,
89+
min_views=min_views,
90+
min_syncs=min_syncs,
91+
sort=sort,
92+
)
93+
else:
94+
# For test stubs, fallback to async service method
95+
# This won't work in executor, but should work in tests
96+
raise RuntimeError(
97+
"Service doesn't have _result_store and executor can't "
98+
"call async methods"
99+
)
88100
except Exception as e:
89101
elapsed = time.time() - start_time
90102
logger.error(
@@ -94,25 +106,42 @@ def call_db():
94106
)
95107
raise
96108

97-
loop = asyncio.get_event_loop()
98-
try:
99-
items, total_count = await asyncio.wait_for(
100-
loop.run_in_executor(None, call_db),
101-
timeout=25.0, # 25 second timeout (less than Render's 30s)
102-
)
103-
elapsed = time.time() - start_time
104-
logger.info(
105-
f"list_roadmaps: Query completed in {elapsed:.2f}s - "
106-
f"Retrieved {len(items)} items, total={total_count}"
107-
)
108-
except asyncio.TimeoutError:
109-
elapsed = time.time() - start_time
110-
logger.error(
111-
f"list_roadmaps: Database query timed out after {elapsed:.2f}s"
112-
)
113-
raise HTTPException(
114-
status_code=status.HTTP_504_GATEWAY_TIMEOUT,
115-
detail="Database query timed out",
109+
# Check if we have _result_store (production) or need to use async method (tests)
110+
if hasattr(service, "_result_store"):
111+
# Production: Use executor for sync DB call
112+
loop = asyncio.get_event_loop()
113+
try:
114+
items, total_count = await asyncio.wait_for(
115+
loop.run_in_executor(None, call_db),
116+
timeout=25.0, # 25 second timeout (less than Render's 30s)
117+
)
118+
elapsed = time.time() - start_time
119+
logger.info(
120+
f"list_roadmaps: Query completed in {elapsed:.2f}s - "
121+
f"Retrieved {len(items)} items, total={total_count}"
122+
)
123+
except asyncio.TimeoutError:
124+
elapsed = time.time() - start_time
125+
logger.error(
126+
f"list_roadmaps: Database query timed out after {elapsed:.2f}s"
127+
)
128+
raise HTTPException(
129+
status_code=status.HTTP_504_GATEWAY_TIMEOUT,
130+
detail="Database query timed out",
131+
)
132+
else:
133+
# Tests: Use async service method directly
134+
logger.info("list_roadmaps: Using async service method (test mode)")
135+
items, total_count = await service.list_catalog(
136+
page=page,
137+
page_size=page_size,
138+
language=language,
139+
tag=tag,
140+
difficulty=difficulty,
141+
min_rating=min_rating,
142+
min_views=min_views,
143+
min_syncs=min_syncs,
144+
sort=sort,
116145
)
117146

118147
total_pages = math.ceil(total_count / page_size) if total_count > 0 else 0

commitly-backend/app/main.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,50 @@ async def lifespan(app: FastAPI):
4949
except Exception as e:
5050
logger.error(f"Database connection failed: {e}", exc_info=True)
5151
# Don't fail startup, but log the error
52+
53+
# Verify schema: Check if required columns exist
54+
logger.info("Verifying database schema...")
55+
try:
56+
with SessionLocal() as session:
57+
# Check if primary_language column exists
58+
result = session.execute(
59+
text(
60+
"""
61+
SELECT column_name
62+
FROM information_schema.columns
63+
WHERE table_name = 'generated_roadmaps'
64+
AND column_name = 'primary_language'
65+
"""
66+
)
67+
)
68+
column_exists = result.fetchone() is not None
69+
70+
if not column_exists:
71+
logger.warning(
72+
"Required columns missing in generated_roadmaps table. "
73+
"Attempting to run migrations..."
74+
)
75+
try:
76+
from pathlib import Path
77+
from alembic.config import Config
78+
from alembic import command
79+
80+
project_root = Path(__file__).parent.parent
81+
alembic_ini = project_root / "alembic.ini"
82+
alembic_cfg = Config(str(alembic_ini))
83+
command.upgrade(alembic_cfg, "head")
84+
logger.info("Migrations applied successfully")
85+
except Exception as migration_error:
86+
logger.error(
87+
f"Failed to run migrations: {migration_error}",
88+
exc_info=True,
89+
)
90+
else:
91+
logger.info("Database schema verified successfully")
92+
except Exception as e:
93+
logger.error(f"Schema verification failed: {e}", exc_info=True)
94+
# Don't fail startup, but log the error
95+
5296
yield
5397
# Shutdown
5498
logger.info("Application shutting down")

0 commit comments

Comments
 (0)