Files
vulncheck/alembic/env.py
T
vulncheck 21f85e4b89 fix(alembic): unbreak fresh installs at migration 028
A fresh `docker compose up` never came up. Migration 028 imported the
live ORM (`from app.models.asset import Asset`) and ran `db.query(Asset)`
against a database that is only at revision 028, so the SELECT asked for
`assets.intune_device_id` — a column revision 033 adds. Alembic aborted
with UndefinedColumn, the whole migration chain rolled back (one
transaction for all revisions), and the backend crash-looped against an
empty database.

028 is now raw SQL: one CTE that flips the legacy Nessus rows and writes
the audit entries, comparing `source::text` / `status::text` so it no
longer depends on today's enum labels either.

env.py switches to transaction_per_migration. 027 adds the
ASSET_DEACTIVATED enum label that 028 inserts, and PostgreSQL refuses to
use a new enum value in the transaction that added it ("unsafe use of new
value"). Per-migration transactions also make a failed upgrade resumable
instead of all-or-nothing.

Verified end to end from a clean `git archive` checkout: migrations run
027 -> 046, default admin is created, backend reports healthy, login
through the frontend proxy returns a session cookie.

tests/test_migrations_no_orm_imports.py locks the bug class down: no
migration may import app code, because the ORM mapping is always newer
than the schema of the revision it runs at.
2026-08-26 15:48:37 +02:00

88 lines
2.4 KiB
Python

"""
Alembic Migration Environment
"""
import os
import sys
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context
# Füge das Projekt-Root zum Python-Path hinzu
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
# Import der Models und Database Config
from app.models.base import Base
from app.database import DATABASE_URL
import app.models # Importiert alle Models
# Alembic Config Objekt
config = context.config
# Override der sqlalchemy.url mit der aus Environment
if DATABASE_URL:
config.set_main_option("sqlalchemy.url", DATABASE_URL)
# Logging Setup
if config.config_file_name is not None:
fileConfig(config.config_file_name)
# MetaData für Autogenerate
target_metadata = Base.metadata
def run_migrations_offline() -> None:
"""
Run migrations in 'offline' mode.
Generiert SQL-Statements ohne DB-Connection.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online() -> None:
"""
Run migrations in 'online' mode.
Führt Migrationen direkt auf der Datenbank aus.
"""
connectable = engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata,
# One transaction PER migration, not one for the whole chain.
# PostgreSQL refuses to *use* an enum label in the same
# transaction that added it ("unsafe use of new value ... of
# enum type"), so any `ALTER TYPE ... ADD VALUE` in revision N
# followed by an INSERT of that label in revision N+1 fails on
# a fresh install. Per-migration transactions also make a
# failed upgrade resumable instead of all-or-nothing.
transaction_per_migration=True,
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()