- Removed outdated and redundant Alembic migration files to streamline the migration directory. This improves maintainability and eliminates duplicate or unused scripts.
161 lines
4.9 KiB
Python
161 lines
4.9 KiB
Python
import sys
|
|
from logging.config import fileConfig
|
|
from pathlib import Path
|
|
|
|
from alembic import context
|
|
from sqlalchemy import create_engine, engine_from_config, pool, text
|
|
from sqlalchemy.engine.url import make_url
|
|
from sqlalchemy.exc import OperationalError
|
|
|
|
# Get the path to the app directory (parent of 'alembic')
|
|
app_dir = Path(__file__).resolve().parent.parent
|
|
# Add the app directory to Python path
|
|
sys.path.append(str(app_dir.parent))
|
|
|
|
# Import Core modules
|
|
from app.core.config import settings
|
|
|
|
# Import all models to ensure they're registered with SQLAlchemy
|
|
from app.models import *
|
|
|
|
# this is the Alembic Config object, which provides
|
|
# access to the values within the .ini file in use.
|
|
config = context.config
|
|
|
|
|
|
def include_object(object, name, type_, reflected, compare_to):
|
|
"""
|
|
Filter objects for autogenerate.
|
|
|
|
Skip comparing functional indexes (like LOWER(column)) and partial indexes
|
|
(with WHERE clauses) as Alembic cannot reliably detect these from models.
|
|
These should be managed manually via dedicated performance migrations.
|
|
|
|
Convention: Any index starting with "ix_perf_" is automatically excluded.
|
|
This allows adding new performance indexes without updating this file.
|
|
"""
|
|
if type_ == "index" and name:
|
|
# Convention-based: any index prefixed with ix_perf_ is manual
|
|
if name.startswith("ix_perf_"):
|
|
return False
|
|
return True
|
|
|
|
# Interpret the config file for Python logging.
|
|
# This line sets up loggers basically.
|
|
if config.config_file_name is not None:
|
|
fileConfig(config.config_file_name)
|
|
|
|
# add your model's MetaData object here
|
|
# for 'autogenerate' support
|
|
target_metadata = Base.metadata
|
|
|
|
# Override the SQLAlchemy URL with the one from settings
|
|
config.set_main_option("sqlalchemy.url", settings.database_url)
|
|
|
|
|
|
def ensure_database_exists(db_url: str) -> None:
|
|
"""
|
|
Ensure the target PostgreSQL database exists.
|
|
If connection to the target DB fails because it doesn't exist, connect to the
|
|
default 'postgres' database and create it. Safe to call multiple times.
|
|
"""
|
|
try:
|
|
# First, try connecting to the target database
|
|
test_engine = create_engine(db_url, poolclass=pool.NullPool)
|
|
with test_engine.connect() as conn:
|
|
conn.execute(text("SELECT 1"))
|
|
test_engine.dispose()
|
|
return
|
|
except OperationalError:
|
|
# Likely the database does not exist; proceed to create it
|
|
pass
|
|
|
|
url = make_url(db_url)
|
|
# Only handle PostgreSQL here
|
|
if url.get_backend_name() != "postgresql":
|
|
return
|
|
|
|
target_db = url.database
|
|
if not target_db:
|
|
return
|
|
|
|
# Build admin URL pointing to the default 'postgres' database
|
|
admin_url = url.set(database="postgres")
|
|
|
|
# CREATE DATABASE cannot run inside a transaction
|
|
admin_engine = create_engine(
|
|
str(admin_url), isolation_level="AUTOCOMMIT", poolclass=pool.NullPool
|
|
)
|
|
try:
|
|
with admin_engine.connect() as conn:
|
|
exists = conn.execute(
|
|
text("SELECT 1 FROM pg_database WHERE datname = :dbname"),
|
|
{"dbname": target_db},
|
|
).scalar()
|
|
if not exists:
|
|
# Quote the database name safely
|
|
dbname_quoted = '"' + target_db.replace('"', '""') + '"'
|
|
conn.execute(text(f"CREATE DATABASE {dbname_quoted}"))
|
|
finally:
|
|
admin_engine.dispose()
|
|
|
|
|
|
def run_migrations_offline() -> None:
|
|
"""Run migrations in 'offline' mode.
|
|
|
|
This configures the context with just a URL
|
|
and not an Engine, though an Engine is acceptable
|
|
here as well. By skipping the Engine creation
|
|
we don't even need a DBAPI to be available.
|
|
|
|
Calls to context.execute() here emit the given string to the
|
|
script output.
|
|
|
|
"""
|
|
url = config.get_main_option("sqlalchemy.url")
|
|
context.configure(
|
|
url=url,
|
|
target_metadata=target_metadata,
|
|
literal_binds=True,
|
|
dialect_opts={"paramstyle": "named"},
|
|
compare_type=True,
|
|
include_object=include_object,
|
|
)
|
|
|
|
with context.begin_transaction():
|
|
context.run_migrations()
|
|
|
|
|
|
def run_migrations_online() -> None:
|
|
"""Run migrations in 'online' mode.
|
|
|
|
In this scenario we need to create an Engine
|
|
and associate a connection with the context.
|
|
|
|
"""
|
|
# Ensure the target database exists (handles first-run cases)
|
|
ensure_database_exists(settings.database_url)
|
|
|
|
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,
|
|
compare_type=True,
|
|
include_object=include_object,
|
|
)
|
|
|
|
with context.begin_transaction():
|
|
context.run_migrations()
|
|
|
|
|
|
if context.is_offline_mode():
|
|
run_migrations_offline()
|
|
else:
|
|
run_migrations_online()
|