From 04a64ff42485e00212d45eec495239e103b380c9 Mon Sep 17 00:00:00 2001 From: Automated Action Date: Sun, 1 Jun 2025 10:30:40 +0000 Subject: [PATCH] Fix database migration error and add error handling --- alembic.ini | 8 ++--- app/db/init_db.py | 84 ++++++++++++++++++++++++++++++++++++++++++++--- main.py | 36 +++++++++++++++++++- migrations/env.py | 29 ++++++++++++---- 4 files changed, 142 insertions(+), 15 deletions(-) diff --git a/alembic.ini b/alembic.ini index cbedf32..c32638a 100644 --- a/alembic.ini +++ b/alembic.ini @@ -35,6 +35,9 @@ script_location = migrations # are written from script.py.mako # output_encoding = utf-8 +# Use absolute path for SQLite URL +sqlalchemy.url = sqlite:////app/storage/db/db.sqlite + # Logging configuration [loggers] keys = root,sqlalchemy,alembic @@ -68,7 +71,4 @@ formatter = generic [formatter_generic] format = %(levelname)-5.5s [%(name)s] %(message)s -datefmt = %H:%M:%S - -# Use absolute path for SQLite URL -sqlalchemy.url = sqlite:////app/storage/db/db.sqlite \ No newline at end of file +datefmt = %H:%M:%S \ No newline at end of file diff --git a/app/db/init_db.py b/app/db/init_db.py index 7474beb..f135ef2 100644 --- a/app/db/init_db.py +++ b/app/db/init_db.py @@ -1,17 +1,93 @@ import logging +import os +from pathlib import Path +from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.orm import Session + +from app.core.config import settings from app.db.session import Base, engine +from app.models import Category, Item, User # Will be implemented later logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) +def ensure_db_dir_exists() -> None: + """ + Ensure the database directory exists. + """ + try: + settings.DB_DIR.mkdir(parents=True, exist_ok=True) + logger.info(f"Database directory ensured at {settings.DB_DIR}") + except PermissionError: + logger.error(f"Permission denied when creating directory {settings.DB_DIR}") + raise + except Exception as e: + logger.error(f"Error creating database directory: {str(e)}") + raise + + def init_db() -> None: """ Initialize the database. Creates all tables defined in the models. """ - # Create tables - logger.info("Creating database tables") - Base.metadata.create_all(bind=engine) - logger.info("Database tables created") \ No newline at end of file + try: + # Ensure the database directory exists + ensure_db_dir_exists() + + # Check if we can access the database file path + db_file = Path(settings.SQLALCHEMY_DATABASE_URL.replace("sqlite:///", "")) + db_dir = db_file.parent + + if not db_dir.exists(): + logger.warning(f"Database directory does not exist: {db_dir}") + db_dir.mkdir(parents=True, exist_ok=True) + logger.info(f"Created database directory: {db_dir}") + + # Create tables + logger.info("Creating database tables") + Base.metadata.create_all(bind=engine) + logger.info("Database tables created") + + except SQLAlchemyError as e: + logger.error(f"SQLAlchemy error during database initialization: {str(e)}") + raise + except Exception as e: + logger.error(f"Unexpected error during database initialization: {str(e)}") + raise + + +def create_first_superuser(db: Session) -> None: + """ + Create the first superuser if it doesn't exist. + This can be called after init_db() to ensure there's at least one admin user. + """ + from app.core.security import get_password_hash + from app.crud.user import user + from app.schemas.user import UserCreate + + # Check if there are any users in the database + first_user = db.query(User).first() + if not first_user: + logger.info("No users found. Creating initial superuser...") + try: + # Get superuser credentials from environment variables or use defaults + admin_email = os.getenv("FIRST_SUPERUSER_EMAIL", "admin@example.com") + admin_password = os.getenv("FIRST_SUPERUSER_PASSWORD", "adminpassword") + + # Create user + user_in = UserCreate( + email=admin_email, + password=admin_password, + is_superuser=True, + full_name="Admin" + ) + user.create(db, obj_in=user_in) + logger.info(f"Initial superuser created with email: {admin_email}") + except Exception as e: + logger.error(f"Error creating initial superuser: {str(e)}") + raise + else: + logger.info("Users exist in the database. Skipping superuser creation.") \ No newline at end of file diff --git a/main.py b/main.py index 8cbaa1f..8cf0055 100644 --- a/main.py +++ b/main.py @@ -1,9 +1,15 @@ +import logging + from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from app.api.v1.api import api_router from app.core.config import settings +from app.db.init_db import ensure_db_dir_exists, init_db + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) # Create FastAPI app app = FastAPI( @@ -24,13 +30,41 @@ if settings.BACKEND_CORS_ORIGINS: # Include API router app.include_router(api_router, prefix=settings.API_V1_STR) + +# Startup event to initialize database +@app.on_event("startup") +async def startup_db_client(): + try: + logger.info("Ensuring database directory exists") + ensure_db_dir_exists() + logger.info("Database directory check completed") + except Exception as e: + logger.error(f"Failed to initialize database directory: {str(e)}") + # We don't want to crash the app if this fails, just log the error + + # Health check endpoint @app.get("/health", response_model=None) async def health_check(): """ Health check endpoint to verify the API is running. """ - return JSONResponse(content={"status": "healthy"}) + # Check database directory + try: + db_dir_exists = settings.DB_DIR.exists() + except Exception: + db_dir_exists = False + + health_status = { + "status": "healthy", + "api": "ok", + "db_directory": "ok" if db_dir_exists else "missing", + } + + if not db_dir_exists: + health_status["status"] = "degraded" + + return JSONResponse(content=health_status) if __name__ == "__main__": diff --git a/migrations/env.py b/migrations/env.py index 8dbe4d0..ed3e219 100644 --- a/migrations/env.py +++ b/migrations/env.py @@ -9,6 +9,7 @@ from sqlalchemy import engine_from_config, pool sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) # Import the SQLAlchemy metadata object +from app.core.config import settings from app.db.session import Base from app.models import * # Import all models for Alembic to detect them @@ -16,6 +17,10 @@ from app.models import * # Import all models for Alembic to detect them # access to the values within the .ini file in use. config = context.config +# Set the sqlalchemy url from the app config if not already set +if not config.get_main_option("sqlalchemy.url"): + config.set_main_option("sqlalchemy.url", settings.SQLALCHEMY_DATABASE_URL) + # Interpret the config file for Python logging. # This line sets up loggers basically. fileConfig(config.config_file_name) @@ -24,11 +29,6 @@ fileConfig(config.config_file_name) # for 'autogenerate' support 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. - def run_migrations_offline(): """Run migrations in 'offline' mode. @@ -43,6 +43,9 @@ def run_migrations_offline(): """ url = config.get_main_option("sqlalchemy.url") + if not url: + raise ValueError("Database URL is not configured. Check alembic.ini or environment variables.") + context.configure( url=url, target_metadata=target_metadata, @@ -61,8 +64,22 @@ def run_migrations_online(): and associate a connection with the context. """ + # Ensure we have a URL configured + url = config.get_main_option("sqlalchemy.url") + if not url: + raise ValueError("Database URL is not configured. Check alembic.ini or environment variables.") + + # Build configuration dictionary + alembic_config = config.get_section(config.config_ini_section) + if not alembic_config: + alembic_config = {} + + # Ensure sqlalchemy.url is in the config + alembic_config["sqlalchemy.url"] = url + + # Create engine connectable = engine_from_config( - config.get_section(config.config_ini_section), + alembic_config, prefix="sqlalchemy.", poolclass=pool.NullPool, )