fix(migrations): schema base 000 per DB vuoti, stamp head dopo create_all (#6)

upgrade head partiva dalla 001, che sa solo aggiungere colonne a tabelle
che assume gia' esistenti: su un DB davvero vuoto falliva subito. Nuova
000_base_schema.py (revision "000_base", down_revision None) crea lo
schema pre-001 esplicitamente (create_table, non Base.metadata.create_all,
che porterebbe gia' le colonne di 001-013 e farebbe fallire 001 su
"Duplicate column"). No-op se la tabella recipes esiste gia'
(sa.inspect(bind).has_table("recipes")). 001 ora ha down_revision =
"000_base".

setup.py: init-db e reset-db, dopo create_all/drop_all+create_all,
eseguono l'equivalente di "alembic stamp head" (nuovo _stamp_head(),
command.stamp con la config di src/backend/migrations/alembic.ini) cosi'
un DB nato da create_all non ritenta le migrazioni 001-013 al prossimo
deploy.

Test: catena upgrade head su SQLite vuoto, tabelle/colonne confrontate
con Base.metadata; DB con recipes gia' nello stato pre-001 -> upgrade
head non fallisce (000 no-op). _stamp_head si appoggia a
migrations/env.py, che costruisce una connessione propria da
settings.database_url (non modificabile dai test tramite l'engine
patchato di setup.py): nei test di setup.py e' quindi no-op via
monkeypatch, cosi' init-db/reset-db continuano a girare su SQLite senza
toccare un MySQL vero.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-21 22:49:38 +02:00
parent 805f41c757
commit e20299548f
6 changed files with 388 additions and 2 deletions
+19 -1
View File
@@ -9,6 +9,8 @@ import secrets
from datetime import datetime, timedelta
from pathlib import Path
from alembic import command
from alembic.config import Config
from fastapi import APIRouter, HTTPException, Request, status
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
@@ -28,10 +30,24 @@ from src.backend.services.measurement_service import calculate_pass_fail
router = APIRouter(prefix="/api/setup", tags=["setup"])
_templates_dir = Path(__file__).resolve().parent.parent.parent / "templates"
_backend_dir = Path(__file__).resolve().parent.parent.parent
_templates_dir = _backend_dir / "templates"
_alembic_ini = _backend_dir / "migrations" / "alembic.ini"
templates = Jinja2Templates(directory=str(_templates_dir))
def _stamp_head() -> None:
"""Mark the schema as fully migrated - the equivalent of `alembic stamp
head` - after `Base.metadata.create_all()`.
A database that got its tables this way instead of through
`upgrade head` has no row in `alembic_version`; without this, its next
deploy would try to replay migrations 001-013 and fail on a column that
is already there (see migration 000_base_schema.py).
"""
command.stamp(Config(str(_alembic_ini)), "head")
# ---------------------------------------------------------------------------
# Request schemas
# ---------------------------------------------------------------------------
@@ -296,6 +312,7 @@ async def init_db(body: PasswordBody):
await _require_not_initialised()
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
_stamp_head()
return {"status": "ok", "message": "Database tables created successfully"}
@@ -734,5 +751,6 @@ async def reset_db(body: ResetDbBody):
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
await conn.run_sync(Base.metadata.create_all)
_stamp_head()
return {"status": "ok", "message": "Database reset successfully"}