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.
88 lines
2.4 KiB
Python
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()
|