diff --git a/backend/alembic.ini b/backend/alembic.ini index 8794f31..f007b71 100644 --- a/backend/alembic.ini +++ b/backend/alembic.ini @@ -1,5 +1,5 @@ [alembic] -script_location = alembic +script_location = app/alembic sqlalchemy.url = postgresql://postgres:postgres@db:5432/eventspace [loggers] diff --git a/backend/alembic/README b/backend/app/alembic/README similarity index 100% rename from backend/alembic/README rename to backend/app/alembic/README diff --git a/backend/alembic/env.py b/backend/app/alembic/env.py similarity index 75% rename from backend/alembic/env.py rename to backend/app/alembic/env.py index 36112a3..5b616e5 100644 --- a/backend/alembic/env.py +++ b/backend/app/alembic/env.py @@ -1,10 +1,24 @@ +import sys from logging.config import fileConfig +from pathlib import Path from sqlalchemy import engine_from_config from sqlalchemy import pool 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 # access to the values within the .ini file in use. config = context.config @@ -16,14 +30,10 @@ if config.config_file_name is not None: # add your model's MetaData object here # for 'autogenerate' support -# from myapp import mymodel -# target_metadata = mymodel.Base.metadata -target_metadata = None +target_metadata = Base.metadata -# other values from the config, defined by the needs of env.py, -# can be acquired: -# my_important_option = config.get_main_option("my_important_option") -# ... etc. +# Override the SQLAlchemy URL with the one from settings +config.set_main_option("sqlalchemy.url", settings.database_url) def run_migrations_offline() -> None: @@ -75,4 +85,4 @@ def run_migrations_online() -> None: if context.is_offline_mode(): run_migrations_offline() else: - run_migrations_online() + run_migrations_online() \ No newline at end of file diff --git a/backend/alembic/script.py.mako b/backend/app/alembic/script.py.mako similarity index 100% rename from backend/alembic/script.py.mako rename to backend/app/alembic/script.py.mako diff --git a/backend/app/main.py b/backend/app/main.py index 755f7cb..25680a2 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -2,7 +2,7 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import HTMLResponse -from app.config import settings +from app.core.config import settings import logging diff --git a/backend/migrate.py b/backend/migrate.py new file mode 100644 index 0000000..fd04012 --- /dev/null +++ b/backend/migrate.py @@ -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()