# File: migrations/env.py import os import sys import warnings import json import importlib from logging.config import fileConfig from sqlalchemy import create_engine, pool from alembic import context # ─── Suppress harmless warnings about FK cycles ─────────────────────────────── warnings.filterwarnings( "ignore", r"Cannot correctly sort tables; there are unresolvable cycles between tables.*" ) # ─── Ensure project root is on sys.path ────────────────────────────────────── project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) sys.path.insert(0, project_root) # ─── Load environment vars so DB URL is available ──────────────────────────── from dotenv import load_dotenv, find_dotenv dotenv = find_dotenv() if dotenv: load_dotenv(dotenv, override=True) # ─── Dynamically import every plugin’s models *before* capturing metadata ──── plugins_dir = os.path.join(project_root, "plugins") for plugin in sorted(os.listdir(plugins_dir)): manifest = os.path.join(plugins_dir, plugin, "plugin.json") if not os.path.isfile(manifest): continue try: meta = json.load(open(manifest)) except Exception: continue for model_mod in meta.get("models", []): try: importlib.import_module(model_mod) except ImportError: pass for sp in meta.get("subplugins", []): for model_mod in sp.get("models", []): try: importlib.import_module(model_mod) except ImportError: pass # ─── Alembic config & logging ──────────────────────────────────────────────── config = context.config fileConfig(config.config_file_name) # ─── Now import the application’s metadata ─────────────────────────────────── from app import db target_metadata = db.metadata # ─── Hook to skip unwanted objects (never drop tables) ─────────────────────── def include_object(obj, name, type_, reflected, compare_to): # skip tables present in DB but not in models if type_ == "table" and reflected and compare_to is None: return False # skip constraints & indexes if type_ in ("foreign_key_constraint", "unique_constraint", "index"): return False return True # ─── Helper to build the DB URL ─────────────────────────────────────────────── def get_url(): url = config.get_main_option("sqlalchemy.url") if url: return url.strip() url = os.environ.get("DATABASE_URL") if url: return url u = os.environ.get("MYSQL_USER") p = os.environ.get("MYSQL_PASSWORD") h = os.environ.get("MYSQL_HOST", "db") pt= os.environ.get("MYSQL_PORT", "3306") dbn= os.environ.get("MYSQL_DATABASE") if u and p and dbn: return f"mysql+pymysql://{u}:{p}@{h}:{pt}/{dbn}" raise RuntimeError("No DB URL configured") # ─── Offline migrations ────────────────────────────────────────────────────── def run_migrations_offline(): context.configure( url=get_url(), target_metadata=target_metadata, literal_binds=True, dialect_opts={"paramstyle": "named"}, compare_type=True, compare_server_default=True, include_object=include_object, ) with context.begin_transaction(): context.run_migrations() # ─── Online migrations ─────────────────────────────────────────────────────── def run_migrations_online(): engine = create_engine(get_url(), poolclass=pool.NullPool) with engine.connect() as conn: context.configure( connection=conn, target_metadata=target_metadata, compare_type=True, compare_server_default=True, include_object=include_object, ) with context.begin_transaction(): context.run_migrations() # ─── Entrypoint ───────────────────────────────────────────────────────────── if context.is_offline_mode(): run_migrations_offline() else: run_migrations_online()