Refactor Alembic setup and add migration helper script.

Updated Alembic configuration and folder structure to reference the `app` module. Introduced a new `migrate.py` script to manage migrations efficiently with commands for generating, applying, and inspecting migrations. Adjusted `env.py` to ensure proper model imports and use environment-driven database URLs.
This commit is contained in:
2025-02-28 09:22:05 +01:00
parent dfeb10c351
commit a60eb045b4
6 changed files with 220 additions and 10 deletions

View File

@@ -1,5 +1,5 @@
[alembic] [alembic]
script_location = alembic script_location = app/alembic
sqlalchemy.url = postgresql://postgres:postgres@db:5432/eventspace sqlalchemy.url = postgresql://postgres:postgres@db:5432/eventspace
[loggers] [loggers]

View File

@@ -1,10 +1,24 @@
import sys
from logging.config import fileConfig from logging.config import fileConfig
from pathlib import Path
from sqlalchemy import engine_from_config from sqlalchemy import engine_from_config
from sqlalchemy import pool from sqlalchemy import pool
from alembic import context from alembic import context
# 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
from app.core.database import Base
# Import all models to ensure they're registered with SQLAlchemy
from app.models import *
# this is the Alembic Config object, which provides # this is the Alembic Config object, which provides
# access to the values within the .ini file in use. # access to the values within the .ini file in use.
config = context.config config = context.config
@@ -16,14 +30,10 @@ if config.config_file_name is not None:
# add your model's MetaData object here # add your model's MetaData object here
# for 'autogenerate' support # for 'autogenerate' support
# from myapp import mymodel target_metadata = Base.metadata
# target_metadata = mymodel.Base.metadata
target_metadata = None
# other values from the config, defined by the needs of env.py, # Override the SQLAlchemy URL with the one from settings
# can be acquired: config.set_main_option("sqlalchemy.url", settings.database_url)
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline() -> None: def run_migrations_offline() -> None:
@@ -75,4 +85,4 @@ def run_migrations_online() -> None:
if context.is_offline_mode(): if context.is_offline_mode():
run_migrations_offline() run_migrations_offline()
else: else:
run_migrations_online() run_migrations_online()

View File

@@ -2,7 +2,7 @@ from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import HTMLResponse from fastapi.responses import HTMLResponse
from app.config import settings from app.core.config import settings
import logging import logging

200
backend/migrate.py Normal file
View File

@@ -0,0 +1,200 @@
#!/usr/bin/env python
"""
Migration script for EventSpace.
Helps with generating and applying Alembic migrations.
"""
import argparse
import subprocess
import sys
from pathlib import Path
# Ensure the project root is in the Python path
project_root = Path(__file__).resolve().parent
if str(project_root) not in sys.path:
sys.path.append(str(project_root))
try:
# Import settings to check if configuration is working
from app.core.config import settings
print(f"Using database URL: {settings.database_url}")
except ImportError as e:
print(f"Error importing settings: {e}")
print("Make sure your Python path includes the project root.")
sys.exit(1)
def check_models():
"""Check if all models are properly imported"""
print("Checking model imports...")
try:
# Import all models through the models package
from app.models import __all__ as all_models
print(f"Found {len(all_models)} model(s):")
for model in all_models:
print(f" - {model}")
return True
except Exception as e:
print(f"Error checking models: {e}")
return False
def generate_migration(message):
"""Generate an Alembic migration with the given message"""
print(f"Generating migration: {message}")
cmd = ["alembic", "revision", "--autogenerate", "-m", message]
result = subprocess.run(cmd, capture_output=True, text=True)
print(result.stdout)
if result.returncode != 0:
print("Error generating migration:", file=sys.stderr)
print(result.stderr, file=sys.stderr)
return False
# Extract revision ID if possible
revision = None
for line in result.stdout.split("\n"):
if "Generating" in line and "..." in line:
try:
# Look for the revision ID, which is typically 12 hex characters
parts = line.split()
for part in parts:
if len(part) >= 12 and all(c in "0123456789abcdef" for c in part[:12]):
revision = part[:12]
break
except Exception:
pass
if revision:
print(f"Generated revision: {revision}")
else:
print("Generated migration (revision ID not identified)")
return revision or True
def apply_migration(revision=None):
"""Apply migrations up to the specified revision or head"""
target = revision or "head"
print(f"Applying migration(s) to: {target}")
cmd = ["alembic", "upgrade", target]
result = subprocess.run(cmd, capture_output=True, text=True)
print(result.stdout)
if result.returncode != 0:
print("Error applying migration:", file=sys.stderr)
print(result.stderr, file=sys.stderr)
return False
print("Migration(s) applied successfully")
return True
def show_current():
"""Show current revision"""
print("Current database revision:")
cmd = ["alembic", "current"]
result = subprocess.run(cmd, capture_output=True, text=True)
print(result.stdout)
if result.returncode != 0:
print("Error getting current revision:", file=sys.stderr)
print(result.stderr, file=sys.stderr)
return False
return True
def list_migrations():
"""List all migrations and their status"""
print("Listing migrations:")
cmd = ["alembic", "history", "--verbose"]
result = subprocess.run(cmd, capture_output=True, text=True)
print(result.stdout)
if result.returncode != 0:
print("Error listing migrations:", file=sys.stderr)
print(result.stderr, file=sys.stderr)
return False
return True
def check_database_connection():
"""Check if database is accessible"""
from sqlalchemy import create_engine
from sqlalchemy.exc import SQLAlchemyError
try:
engine = create_engine(settings.database_url)
with engine.connect() as conn:
print("Database connection successful!")
return True
except SQLAlchemyError as e:
print(f"Error connecting to database: {e}")
return False
def main():
"""Main function"""
parser = argparse.ArgumentParser(description='EventSpace database migration helper')
subparsers = parser.add_subparsers(dest='command', help='Command to run')
# Generate command
generate_parser = subparsers.add_parser('generate', help='Generate a migration')
generate_parser.add_argument('message', help='Migration message')
# Apply command
apply_parser = subparsers.add_parser('apply', help='Apply migrations')
apply_parser.add_argument('--revision', help='Specific revision to apply to')
# List command
subparsers.add_parser('list', help='List migrations')
# Current command
subparsers.add_parser('current', help='Show current revision')
# Check command
subparsers.add_parser('check', help='Check database connection and models')
# Auto command (generate and apply)
auto_parser = subparsers.add_parser('auto', help='Generate and apply migration')
auto_parser.add_argument('message', help='Migration message')
args = parser.parse_args()
if args.command == 'generate':
check_models()
generate_migration(args.message)
elif args.command == 'apply':
apply_migration(args.revision)
elif args.command == 'list':
list_migrations()
elif args.command == 'current':
show_current()
elif args.command == 'check':
check_database_connection()
check_models()
elif args.command == 'auto':
check_models()
revision = generate_migration(args.message)
if revision:
proceed = input("Press Enter to apply migration or Ctrl+C to abort... ")
apply_migration()
else:
parser.print_help()
if __name__ == "__main__":
main()