This commit is contained in:
2025-07-09 01:05:45 -05:00
parent 1bbe6e2743
commit d7a610a83b
113 changed files with 1512 additions and 2348 deletions

View File

@ -2,32 +2,30 @@
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
# ─── Ensure we can load .env and app code ────────────────────────────────────
# ─── 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 .env (so MYSQL_* and other vars are available) ─────────────────────
# ─── Load environment vars so DB URL is available ────────────────────────────
from dotenv import load_dotenv, find_dotenv
dotenv_path = find_dotenv() # looks in project root or parents
if dotenv_path:
load_dotenv(dotenv_path, override=True)
dotenv = find_dotenv()
if dotenv:
load_dotenv(dotenv, override=True)
# ─── Alembic Config & Logging ────────────────────────────────────────────────
config = context.config
fileConfig(config.config_file_name)
# ─── Import your apps metadata for 'autogenerate' support ─────────────────
from app import db
target_metadata = db.metadata
# ─── Dynamically import all plugin models listed in plugin.json ─────────────
# ─── Dynamically import every plugins 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")
@ -37,7 +35,6 @@ for plugin in sorted(os.listdir(plugins_dir)):
meta = json.load(open(manifest))
except Exception:
continue
for model_mod in meta.get("models", []):
try:
importlib.import_module(model_mod)
@ -50,59 +47,70 @@ for plugin in sorted(os.listdir(plugins_dir)):
except ImportError:
pass
# ─── Build or retrieve the database URL ──────────────────────────────────────
def get_database_url():
# 1) alembic.ini setting
# ─── Alembic config & logging ────────────────────────────────────────────────
config = context.config
fileConfig(config.config_file_name)
# ─── Now import the applications 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
# 2) Generic DATABASE_URL env var
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")
# 3) MySQL env vars (from .env or docker-compose)
user = os.environ.get("MYSQL_USER")
pwd = os.environ.get("MYSQL_PASSWORD")
host = os.environ.get("MYSQL_HOST", "db")
port = os.environ.get("MYSQL_PORT", "3306")
dbn = os.environ.get("MYSQL_DATABASE")
if user and pwd and dbn:
return f"mysql+pymysql://{user}:{pwd}@{host}:{port}/{dbn}"
raise RuntimeError(
"Database URL not configured for Alembic migrations; "
"set 'sqlalchemy.url' in alembic.ini, or DATABASE_URL, "
"or MYSQL_USER/MYSQL_PASSWORD/MYSQL_DATABASE in the environment"
)
# ─── Offline migration ───────────────────────────────────────────────────────
# ─── Offline migrations ──────────────────────────────────────────────────────
def run_migrations_offline():
url = get_database_url()
context.configure(
url=url,
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 migration ───────────────────────────────────────────────────────
# ─── Online migrations ───────────────────────────────────────────────────────
def run_migrations_online():
url = get_database_url()
connectable = create_engine(url, poolclass=pool.NullPool)
with connectable.connect() as connection:
engine = create_engine(get_url(), poolclass=pool.NullPool)
with engine.connect() as conn:
context.configure(
connection=connection,
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: