From 06459539517ad4ccf962f1a3b75c8ce99048b84f Mon Sep 17 00:00:00 2001 From: Automated Action Date: Fri, 16 May 2025 06:46:45 +0000 Subject: [PATCH] Implement ultra-reliable database access with comprehensive error handling --- app/api/routers/tasks.py | 272 ++++++++++++++++++++++++--------------- app/core/config.py | 45 +++---- app/db/init_db.py | 183 +++++++++++++++----------- app/db/session.py | 96 +++++++++----- main.py | 228 +++++++++++++++++++++----------- 5 files changed, 512 insertions(+), 312 deletions(-) diff --git a/app/api/routers/tasks.py b/app/api/routers/tasks.py index 7a68f9c..567fda4 100644 --- a/app/api/routers/tasks.py +++ b/app/api/routers/tasks.py @@ -94,136 +94,204 @@ def create_task( task_in: TaskCreate, ) -> Any: """ - Create new task. + Create new task - using direct SQLite approach for reliability. """ import sqlite3 - import json + import time + import sys import traceback from datetime import datetime from app.db.session import db_file - # Print detailed request info for debugging - print(f"[{datetime.now().isoformat()}] Creating task: {task_in}") + # Log creation attempt + print(f"[{datetime.now().isoformat()}] Task creation requested", file=sys.stdout) + # Use direct SQLite for maximum reliability try: - # Handle datetime conversion for due_date - if task_in.due_date: - print(f"Due date before processing: {task_in.due_date}") - if isinstance(task_in.due_date, str): - try: - task_in.due_date = datetime.fromisoformat(task_in.due_date.replace('Z', '+00:00')) - except Exception as e: - print(f"Error parsing due_date: {e}") - # Continue with string, SQLAlchemy will handle it - - # First try the normal SQLAlchemy approach + # Extract task data regardless of Pydantic version try: - print("Attempting to create task via SQLAlchemy...") - task = crud.task.create(db, obj_in=task_in) - print(f"Task created successfully with ID: {task.id}") - return task - except Exception as e: - print(f"SQLAlchemy task creation failed: {e}") - print(traceback.format_exc()) - # Continue to try direct SQLite approach - - # Fallback: Try direct SQLite approach - print("Falling back to direct SQLite task creation...") - try: - # Convert Pydantic model to dict - task_data = task_in.model_dump() if hasattr(task_in, 'model_dump') else task_in.dict() + if hasattr(task_in, 'model_dump'): + task_data = task_in.model_dump() + elif hasattr(task_in, 'dict'): + task_data = task_in.dict() + else: + # Fallback for any case + task_data = { + 'title': getattr(task_in, 'title', 'Untitled Task'), + 'description': getattr(task_in, 'description', ''), + 'priority': getattr(task_in, 'priority', 'medium'), + 'status': getattr(task_in, 'status', 'todo'), + 'due_date': getattr(task_in, 'due_date', None), + 'completed': getattr(task_in, 'completed', False) + } print(f"Task data: {task_data}") - - # Format datetime objects - if task_data.get('due_date'): + except Exception as e: + print(f"Error extracting task data: {e}") + # Fallback to minimal data + task_data = { + 'title': str(getattr(task_in, 'title', 'Unknown Title')), + 'description': str(getattr(task_in, 'description', '')), + 'priority': 'medium', + 'status': 'todo', + 'completed': False + } + + # Format due_date if present + if task_data.get('due_date'): + try: if isinstance(task_data['due_date'], datetime): task_data['due_date'] = task_data['due_date'].isoformat() - - # Connect directly to SQLite and insert the task - conn = sqlite3.connect(str(db_file)) - cursor = conn.cursor() - now = datetime.utcnow().isoformat() - - # Check if task table exists - cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='task'") - if not cursor.fetchone(): - # Create the task table if it doesn't exist + elif isinstance(task_data['due_date'], str): + # Standardize format by parsing and reformatting + parsed_date = datetime.fromisoformat( + task_data['due_date'].replace('Z', '+00:00') + ) + task_data['due_date'] = parsed_date.isoformat() + except Exception as e: + print(f"Warning: Could not parse due_date: {e}") + # Keep as-is or set to None if invalid + if not isinstance(task_data['due_date'], str): + task_data['due_date'] = None + + # Get current timestamp for created/updated fields + now = datetime.utcnow().isoformat() + + # Connect to SQLite with retry logic + for retry in range(3): + conn = None + try: + # Try to connect to the database with a timeout + conn = sqlite3.connect(str(db_file), timeout=30) + cursor = conn.cursor() + + # Create the task table if it doesn't exist - using minimal schema cursor.execute(""" CREATE TABLE IF NOT EXISTS task ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, description TEXT, - priority TEXT, - status TEXT, + priority TEXT DEFAULT 'medium', + status TEXT DEFAULT 'todo', due_date TEXT, - completed INTEGER, - created_at TEXT, - updated_at TEXT + completed INTEGER DEFAULT 0, + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT DEFAULT CURRENT_TIMESTAMP ) """) - print("Created task table with direct SQLite") - - # Insert the task - cursor.execute( - """ - INSERT INTO task ( - title, description, priority, status, - due_date, completed, created_at, updated_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) - """, - ( - task_data.get('title', 'Untitled Task'), - task_data.get('description', ''), - task_data.get('priority', 'medium'), - task_data.get('status', 'todo'), - task_data.get('due_date'), - 1 if task_data.get('completed') else 0, - now, - now + + # Insert the task - provide defaults for all fields + cursor.execute( + """ + INSERT INTO task ( + title, description, priority, status, + due_date, completed, created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + task_data.get('title', 'Untitled'), + task_data.get('description', ''), + task_data.get('priority', 'medium'), + task_data.get('status', 'todo'), + task_data.get('due_date'), + 1 if task_data.get('completed') else 0, + now, + now + ) ) - ) - conn.commit() - task_id = cursor.lastrowid - - # Return the created task - cursor.execute("SELECT * FROM task WHERE id = ?", (task_id,)) - row = cursor.fetchone() - conn.close() - - if row: - column_names = ['id', 'title', 'description', 'priority', 'status', - 'due_date', 'completed', 'created_at', 'updated_at'] - task_dict = {column_names[i]: row[i] for i in range(len(column_names))} - # Convert completed to boolean - task_dict['completed'] = bool(task_dict['completed']) - # Fake Task object with attributes - class TaskResult: - def __init__(self, **kwargs): - for key, value in kwargs.items(): - setattr(self, key, value) + # Get the ID of the inserted task + task_id = cursor.lastrowid + print(f"Task inserted with ID: {task_id}") - print(f"Created task with direct SQLite, ID: {task_id}") - return TaskResult(**task_dict) - else: - raise Exception("Task was created but could not be retrieved") + # Commit the transaction + conn.commit() + + # Retrieve the created task to return it + cursor.execute("SELECT * FROM task WHERE id = ?", (task_id,)) + row = cursor.fetchone() + + if row: + # Get column names from cursor description + column_names = [desc[0] for desc in cursor.description] + + # Create a dictionary from row values + task_dict = dict(zip(column_names, row)) + + # Convert 'completed' to boolean + if 'completed' in task_dict: + task_dict['completed'] = bool(task_dict['completed']) + + # Create an object that mimics the Task model + class TaskResult: + def __init__(self, **kwargs): + for key, value in kwargs.items(): + setattr(self, key, value) + + print(f"Task created successfully: ID={task_id}") + + # Close the connection and return the task + conn.close() + return TaskResult(**task_dict) + else: + conn.close() + raise Exception(f"Task creation succeeded but retrieval failed for ID: {task_id}") + + except sqlite3.OperationalError as e: + if conn: + conn.close() + + # Check if retry is appropriate + if "database is locked" in str(e) and retry < 2: + wait_time = (retry + 1) * 1.5 # Exponential backoff + print(f"Database locked, retrying in {wait_time}s (attempt {retry+1}/3)") + time.sleep(wait_time) + else: + print(f"SQLite operational error: {e}") + raise + + except Exception as e: + if conn: + # Try to rollback if connection is still open + try: + conn.rollback() + except: + pass + conn.close() + + print(f"Error in SQLite task creation: {e}") + print(traceback.format_exc()) + + # Only retry on specific transient errors + if retry < 2 and ("locked" in str(e).lower() or "busy" in str(e).lower()): + time.sleep(1) + continue + raise + + # If we reach here, the retry loop failed + raise Exception("Failed to create task after multiple attempts") + + except Exception as sqlite_error: + # Final fallback: try SQLAlchemy approach + try: + print(f"Direct SQLite approach failed: {sqlite_error}") + print("Trying SQLAlchemy as fallback...") - except Exception as e: - print(f"Direct SQLite task creation failed: {e}") + task = crud.task.create(db, obj_in=task_in) + print(f"Task created with SQLAlchemy fallback: ID={task.id}") + return task + + except Exception as alch_error: + print(f"SQLAlchemy fallback also failed: {alch_error}") print(traceback.format_exc()) + + # Provide detailed error information + error_detail = f"Task creation failed. Primary error: {str(sqlite_error)}. Fallback error: {str(alch_error)}" + print(f"Final error: {error_detail}") + raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"All task creation methods failed. Last error: {str(e)}", + detail=error_detail ) - except HTTPException: - raise - except Exception as e: - print(f"Unhandled error in create_task: {str(e)}") - print(traceback.format_exc()) - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Error creating task: {str(e)}", - ) @router.get("/{task_id}", response_model=Task) diff --git a/app/core/config.py b/app/core/config.py index 01604af..575986c 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -5,33 +5,28 @@ from typing import Any, Dict, List, Optional, Union from pydantic import AnyHttpUrl, field_validator from pydantic_settings import BaseSettings -# Try multiple possible database locations in order of preference -DB_LOCATIONS = [ - Path("/app/storage/db"), - Path("/tmp/taskmanager/db"), - Path.home() / ".taskmanager/db", - Path.cwd() / "db" -] +import os -# Try to find or create a writable database directory -DB_DIR = None -for location in DB_LOCATIONS: - try: - location.mkdir(parents=True, exist_ok=True) - # Check if directory is writable by creating a test file - test_file = location / "test_write" - test_file.touch() - test_file.unlink() # Remove test file - DB_DIR = location - print(f"Using database directory: {DB_DIR}") - break - except Exception as e: - print(f"Could not use {location} for database: {e}") +# Use a simple, universally accessible path for database +# First try environment variable, then local directory +DB_PATH = os.environ.get("DB_PATH") +if DB_PATH: + DB_DIR = Path(DB_PATH) + print(f"Using database path from environment: {DB_DIR}") +else: + # Use 'db' directory in the current working directory + DB_DIR = Path.cwd() / "db" + print(f"Using local database path: {DB_DIR}") -# If no location works, fall back to current directory -if DB_DIR is None: - DB_DIR = Path.cwd() - print(f"Falling back to current directory for database: {DB_DIR}") +# Create database directory +try: + DB_DIR.mkdir(parents=True, exist_ok=True) + print(f"Created or verified database directory: {DB_DIR}") +except Exception as e: + print(f"Error creating database directory: {e}") + # Fall back to /tmp if we can't create our preferred directory + DB_DIR = Path("/tmp") + print(f"Falling back to temporary directory: {DB_DIR}") class Settings(BaseSettings): PROJECT_NAME: str = "Task Manager API" diff --git a/app/db/init_db.py b/app/db/init_db.py index d46ff04..c3f57aa 100644 --- a/app/db/init_db.py +++ b/app/db/init_db.py @@ -15,95 +15,126 @@ from app.core.config import settings def init_db() -> None: """ - Initialize database with required tables directly using both SQLAlchemy and - SQLite native commands for maximum reliability. + Initialize database using both direct SQLite and SQLAlchemy approaches + for maximum reliability. """ print(f"Initializing database at {db_file}") - db_dir = db_file.parent + print(f"Using SQLAlchemy URL: {settings.SQLALCHEMY_DATABASE_URL}") - # Ensure database directory exists + # First try direct SQLite approach to ensure we have a basic database file try: - db_dir.mkdir(parents=True, exist_ok=True) - print(f"Database directory created or already exists: {db_dir}") - except Exception as e: - print(f"Error creating database directory: {e}") - - # First, try to create an empty database file if it doesn't exist - if not db_file.exists(): - try: - # Create an empty file - db_file.touch() - print(f"Created empty database file: {db_file}") - except Exception as e: - print(f"Failed to create database file: {e}") - - # First, try direct SQLite connection to create basic structure - # This bypasses SQLAlchemy entirely for the initial database creation - try: - print(f"Attempting direct SQLite connection to {db_file}") - sqlite_conn = sqlite3.connect(str(db_file)) - sqlite_conn.execute("PRAGMA journal_mode=WAL") - sqlite_conn.execute("CREATE TABLE IF NOT EXISTS _db_init_check (id INTEGER PRIMARY KEY)") - sqlite_conn.execute("INSERT OR IGNORE INTO _db_init_check VALUES (1)") - sqlite_conn.commit() - sqlite_conn.close() - print("Direct SQLite connection and initialization successful") - except Exception as e: - print(f"Direct SQLite initialization error: {e}") - - # Now try with SQLAlchemy - try: - # Try to connect to check if the database is accessible - max_retries = 5 - retry_count = 0 - connected = False + # Ensure database file exists and is writable + with open(db_file, 'a'): # Try opening for append (creates if doesn't exist) + os.utime(db_file, None) # Update access/modify time - while retry_count < max_retries and not connected: - try: - with engine.connect() as conn: - result = conn.execute(text("SELECT 1")).scalar() - print(f"Database connection successful. Test query result: {result}") - connected = True - except Exception as e: - retry_count += 1 - print(f"Database connection error (attempt {retry_count}/{max_retries}): {e}") - time.sleep(1) # Wait a second before retrying + print(f"Database file exists and is writable: {db_file}") - if not connected: - print(f"Failed to connect to database after {max_retries} attempts") - # Continue anyway to see if we can make progress + # Try direct SQLite connection to create task table + conn = sqlite3.connect(str(db_file)) - # Try to create tables - try: - print("Creating database tables with SQLAlchemy...") - Base.metadata.create_all(bind=engine) + # Enable foreign keys and WAL journal mode + conn.execute("PRAGMA foreign_keys = ON") + conn.execute("PRAGMA journal_mode = WAL") + + # Create task table if it doesn't exist + conn.execute(""" + CREATE TABLE IF NOT EXISTS task ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + description TEXT, + priority TEXT DEFAULT 'medium', + status TEXT DEFAULT 'todo', + due_date TEXT, + completed INTEGER DEFAULT 0, + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT DEFAULT CURRENT_TIMESTAMP + ) + """) + + # Create an index on the id column + conn.execute("CREATE INDEX IF NOT EXISTS idx_task_id ON task(id)") + + # Add a sample task if the table is empty + cursor = conn.cursor() + cursor.execute("SELECT COUNT(*) FROM task") + count = cursor.fetchone()[0] + + if count == 0: + now = datetime.utcnow().isoformat() + conn.execute(""" + INSERT INTO task (title, description, priority, status, completed, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?) + """, ( + "Example Task", + "This is an example task created during initialization", + "medium", + "todo", + 0, + now, + now + )) - # Verify tables - inspector = inspect(engine) - tables = inspector.get_table_names() + conn.commit() + cursor.close() + conn.close() + print("Successfully initialized database with direct SQLite") + except Exception as e: + print(f"Error during direct SQLite initialization: {e}") + import traceback + print(traceback.format_exc()) + + # Now try with SQLAlchemy as a backup approach + try: + print("Attempting SQLAlchemy database initialization...") + + # Try to create all tables from models + Base.metadata.create_all(bind=engine) + print("Successfully created tables with SQLAlchemy") + + # Verify tables exist + with engine.connect() as conn: + # Get list of tables + result = conn.execute(text( + "SELECT name FROM sqlite_master WHERE type='table'" + )) + tables = [row[0] for row in result] print(f"Tables in database: {', '.join(tables)}") - if 'task' not in tables: - print("WARNING: 'task' table not created!") - else: - print("'task' table successfully created.") + # Verify task table exists + if 'task' in tables: + # Check if task table is empty + result = conn.execute(text("SELECT COUNT(*) FROM task")) + task_count = result.scalar() + print(f"Task table contains {task_count} records") - except Exception as e: - print(f"Error creating tables: {e}") - - # Print database info for debugging - try: - print(f"Database file size: {os.path.getsize(db_file)} bytes") - print(f"Database file permissions: {oct(os.stat(db_file).st_mode)[-3:]}") - print(f"Database dir permissions: {oct(os.stat(db_dir).st_mode)[-3:]}") - except Exception as e: - print(f"Error getting file info: {e}") - - print("Database initialization completed") + # If table exists but is empty, add a sample task + if task_count == 0: + print("Adding sample task with SQLAlchemy") + from app.models.task import Task, TaskPriority, TaskStatus + sample_task = Task( + title="Sample SQLAlchemy Task", + description="This is a sample task created with SQLAlchemy", + priority=TaskPriority.MEDIUM, + status=TaskStatus.TODO, + completed=False, + created_at=datetime.utcnow(), + updated_at=datetime.utcnow() + ) + from app.db.session import SessionLocal + db = SessionLocal() + db.add(sample_task) + db.commit() + db.close() + print("Added sample task with SQLAlchemy") + else: + print("WARNING: 'task' table not found!") + print("SQLAlchemy database initialization completed") except Exception as e: - print(f"Database initialization error: {str(e)}") - print("Continuing anyway...") + print(f"Error during SQLAlchemy initialization: {e}") + import traceback + print(traceback.format_exc()) + print("Continuing despite SQLAlchemy initialization error...") def create_test_task(): diff --git a/app/db/session.py b/app/db/session.py index aa8edf5..9d35fd9 100644 --- a/app/db/session.py +++ b/app/db/session.py @@ -1,5 +1,6 @@ import os import time +import sqlite3 from typing import Generator from pathlib import Path @@ -9,71 +10,102 @@ from sqlalchemy.exc import OperationalError, SQLAlchemyError from app.core.config import settings, DB_DIR -# Ensure the database directory exists +# Define database filepath - use DB_DIR from config db_file = DB_DIR / "db.sqlite" print(f"Database file path: {db_file}") -# Configure SQLite connection with more robust settings +# Try to touch the database file to ensure it exists +try: + if not db_file.exists(): + # Try to create an empty database file + db_file.touch() + print(f"Database file created or verified: {db_file}") +except Exception as e: + print(f"Warning: Could not create database file: {e}") + +# Configure SQLite connection with simplified, robust settings engine = create_engine( settings.SQLALCHEMY_DATABASE_URL, connect_args={ "check_same_thread": False, "timeout": 30, # Wait up to 30 seconds for the lock }, - pool_recycle=3600, # Recycle connections after 1 hour + # Minimal pool settings for stability pool_pre_ping=True, # Verify connections before usage - pool_size=10, # Maximum 10 connections - max_overflow=20, # Allow up to 20 overflow connections + echo=True, # Log all SQL for debugging ) -# Add SQLite optimizations +# Add essential SQLite optimizations @event.listens_for(engine, "connect") def optimize_sqlite_connection(dbapi_connection, connection_record): - """Configure SQLite connection for better performance and reliability.""" - # Enable WAL journal mode for better concurrency - dbapi_connection.execute("PRAGMA journal_mode=WAL") - # Ensure foreign keys are enforced - dbapi_connection.execute("PRAGMA foreign_keys=ON") - # Synchronous setting for better performance with decent safety - dbapi_connection.execute("PRAGMA synchronous=NORMAL") - # Cache settings - dbapi_connection.execute("PRAGMA cache_size=-64000") # 64MB cache - # Temp storage in memory - dbapi_connection.execute("PRAGMA temp_store=MEMORY") + """Configure SQLite connection for better reliability.""" + try: + # These are essential for SQLite stability + dbapi_connection.execute("PRAGMA journal_mode=WAL") + dbapi_connection.execute("PRAGMA synchronous=NORMAL") + except Exception as e: + print(f"Warning: Could not configure SQLite connection: {e}") +# Simplified Session factory SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) -# More robust database access with retry logic +# More robust database access with retry logic and error printing def get_db() -> Generator: """ Get a database session with retry logic for transient errors. - Creates the database file and tables if they don't exist. """ db = None retries = 3 - retry_delay = 0.5 # Start with 0.5 second delay for attempt in range(retries): try: db = SessionLocal() + # Log connection attempt + print(f"Database connection attempt {attempt+1}") + # Test connection with a simple query db.execute("SELECT 1") - # Yield the working connection + + # Connection succeeded + print("Database connection successful") yield db break - except (OperationalError, SQLAlchemyError) as e: + + except Exception as e: + # Close failed connection if db: db.close() + db = None - # Only retry transient errors like "database is locked" - if attempt < retries - 1: - print(f"Database connection attempt {attempt+1} failed: {e}") - print(f"Retrying in {retry_delay} seconds...") - time.sleep(retry_delay) - retry_delay *= 2 # Exponential backoff - else: - print(f"All database connection attempts failed: {e}") + error_msg = f"Database connection attempt {attempt+1} failed: {e}" + print(error_msg) + + # Log critical error details + import traceback + print(f"Error traceback: {traceback.format_exc()}") + + # Check if we can directly access database + try: + # Try direct sqlite3 connection as a test + direct_conn = sqlite3.connect(str(db_file)) + direct_conn.execute("SELECT 1") + direct_conn.close() + print("Direct SQLite connection succeeded but SQLAlchemy failed") + except Exception as direct_e: + print(f"Direct SQLite connection also failed: {direct_e}") + + # Last attempt - raise the error to return 500 status + if attempt == retries - 1: + print("All database connection attempts failed") raise - finally: + + # Otherwise sleep and retry + time.sleep(1) + + # Always ensure db is closed + try: if db: - db.close() \ No newline at end of file + print("Closing database connection") + db.close() + except Exception as e: + print(f"Error closing database: {e}") \ No newline at end of file diff --git a/main.py b/main.py index f575032..44395f7 100644 --- a/main.py +++ b/main.py @@ -53,16 +53,77 @@ app.add_middleware( allow_headers=["*"], ) -# Add exception handlers for better error reporting +# Add comprehensive exception handlers for better error reporting @app.exception_handler(Exception) -async def validation_exception_handler(request: Request, exc: Exception): +async def global_exception_handler(request: Request, exc: Exception): import traceback + import sys + + # Log the full error with traceback to stdout/stderr + error_tb = traceback.format_exc() + print(f"CRITICAL ERROR: {str(exc)}", file=sys.stderr) + print(f"Request path: {request.url.path}", file=sys.stderr) + print(f"Traceback:\n{error_tb}", file=sys.stderr) + + # Get request info for debugging + headers = dict(request.headers) + # Remove sensitive headers + if 'authorization' in headers: + headers['authorization'] = '[REDACTED]' + if 'cookie' in headers: + headers['cookie'] = '[REDACTED]' + + # Include minimal traceback in response for debugging + tb_lines = error_tb.split('\n') + simplified_tb = [] + for line in tb_lines: + if line and not line.startswith(' '): + simplified_tb.append(line) + + # Create detailed error response error_detail = { - "detail": f"Internal Server Error: {str(exc)}", + "status": "error", + "message": str(exc), "type": str(type(exc).__name__), - "traceback": traceback.format_exc().split("\n") + "path": request.url.path, + "method": request.method, + "traceback_summary": simplified_tb[-10:] if len(simplified_tb) > 10 else simplified_tb, } - print(f"Error processing request: {error_detail}") + + # Add SQLite diagnostic check + try: + import sqlite3 + from app.db.session import db_file + + # Try basic SQLite operations + conn = sqlite3.connect(str(db_file)) + cursor = conn.cursor() + cursor.execute("PRAGMA integrity_check") + integrity = cursor.fetchone()[0] + + # Check if task table exists + cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='task'") + task_table_exists = cursor.fetchone() is not None + + # Get file info + import os + file_exists = os.path.exists(db_file) + file_size = os.path.getsize(db_file) if file_exists else 0 + + # Add SQLite diagnostics to response + error_detail["db_diagnostics"] = { + "file_exists": file_exists, + "file_size": file_size, + "integrity": integrity, + "task_table_exists": task_table_exists, + } + + conn.close() + except Exception as db_error: + error_detail["db_diagnostics"] = {"error": str(db_error)} + + # Return the error response + print(f"Returning error response: {error_detail}") return JSONResponse( status_code=500, content=error_detail, @@ -103,103 +164,116 @@ def test_db_connection(): """ Test database connection and table creation """ - from sqlalchemy import text, inspect - from app.db.session import engine - import traceback, os, subprocess - from app.core.config import DB_DIR - try: - # Check directory structure and permissions - storage_info = { - "app_dir_exists": os.path.exists("/app"), - "app_dir_writable": os.access("/app", os.W_OK), - "storage_dir_exists": os.path.exists("/app/storage"), - "storage_dir_writable": os.access("/app/storage", os.W_OK) if os.path.exists("/app/storage") else False, - "db_dir_exists": os.path.exists(str(DB_DIR)), - "db_dir_writable": os.access(str(DB_DIR), os.W_OK) if os.path.exists(str(DB_DIR)) else False, + import os + import sqlite3 + import traceback + from sqlalchemy import text + from app.db.session import engine, db_file + from app.core.config import DB_DIR + + # First check direct file access + file_info = { + "db_dir": str(DB_DIR), + "db_file": str(db_file), + "exists": os.path.exists(db_file), + "size": os.path.getsize(db_file) if os.path.exists(db_file) else 0, + "writable": os.access(db_file, os.W_OK) if os.path.exists(db_file) else False, + "dir_writable": os.access(DB_DIR, os.W_OK) if os.path.exists(DB_DIR) else False } - # Get disk usage information - disk_usage = {} + # Try direct SQLite connection + sqlite_test = {} try: - df_output = subprocess.check_output(["df", "-h", "/app/storage"]).decode() - disk_usage["df_output"] = df_output.strip().split('\n') + conn = sqlite3.connect(str(db_file)) + sqlite_test["connection"] = "successful" + + # Check if task table exists + cursor = conn.cursor() + cursor.execute("SELECT name FROM sqlite_master WHERE type='table'") + tables = [row[0] for row in cursor.fetchall()] + sqlite_test["tables"] = tables + + # Check for task table specifically + if 'task' in tables: + cursor.execute("SELECT COUNT(*) FROM task") + task_count = cursor.fetchone()[0] + sqlite_test["task_count"] = task_count + + # Get a sample task if available + if task_count > 0: + cursor.execute("SELECT * FROM task LIMIT 1") + column_names = [description[0] for description in cursor.description] + row = cursor.fetchone() + sample_task = dict(zip(column_names, row)) + sqlite_test["sample_task"] = sample_task + + # Check database integrity + cursor.execute("PRAGMA integrity_check") + integrity = cursor.fetchone()[0] + sqlite_test["integrity"] = integrity + + conn.close() except Exception as e: - disk_usage["error"] = str(e) + sqlite_test["connection"] = "failed" + sqlite_test["error"] = str(e) + sqlite_test["traceback"] = traceback.format_exc() - # Try database connection - connection_info = {} - inspector = None + # Try SQLAlchemy connection + sqlalchemy_test = {} try: with engine.connect() as conn: - connection_info["connection"] = "successful" - connection_info["test_query"] = conn.execute(text("SELECT 1")).scalar() - inspector = inspect(engine) - except Exception as e: - connection_info["connection"] = "failed" - connection_info["error"] = str(e) - - # Get table information if connection successful - table_info = {} - if inspector: - tables = inspector.get_table_names() - connection_info["tables"] = tables - - for table in tables: - columns = inspector.get_columns(table) - table_info[table] = [col['name'] for col in columns] + # Basic connectivity test + result = conn.execute(text("SELECT 1")).scalar() + sqlalchemy_test["basic_query"] = result - # Try to query task table if it exists - if 'task' in tables: - with engine.connect() as conn: - try: - task_count = conn.execute(text("SELECT COUNT(*) FROM task")).scalar() - connection_info["task_count"] = task_count - except Exception as e: - connection_info["task_query_error"] = str(e) + # Check tables + result = conn.execute(text("SELECT name FROM sqlite_master WHERE type='table'")) + tables = [row[0] for row in result] + sqlalchemy_test["tables"] = tables + + # Check task table + if 'task' in tables: + result = conn.execute(text("SELECT COUNT(*) FROM task")) + sqlalchemy_test["task_count"] = result.scalar() + except Exception as e: + sqlalchemy_test["connection"] = "failed" + sqlalchemy_test["error"] = str(e) + sqlalchemy_test["traceback"] = traceback.format_exc() - # Database file information - db_file = f"{DB_DIR}/db.sqlite" - db_file_info = { - "path": db_file, - "exists": os.path.exists(db_file), - "size_bytes": os.path.getsize(db_file) if os.path.exists(db_file) else 0, - "writable": os.access(db_file, os.W_OK) if os.path.exists(db_file) else False, + # Check environment + env_info = { + "cwd": os.getcwd(), + "env_variables": {k: v for k, v in os.environ.items() if k.startswith(('DB_', 'SQL', 'PATH'))} } - # SQLAlchemy configuration - from app.core.config import settings - db_config = { - "sqlalchemy_url": settings.SQLALCHEMY_DATABASE_URL, - "connect_args": {"check_same_thread": False} - } - - # Test file creation + # Try to create a test file write_test = {} try: - test_file = f"{DB_DIR}/test_db.txt" - with open(test_file, 'w') as f: - f.write("Test write access") + test_path = DB_DIR / "write_test.txt" + with open(test_path, 'w') as f: + f.write("Test content") write_test["success"] = True - write_test["path"] = test_file - os.remove(test_file) # Clean up + write_test["path"] = str(test_path) + os.unlink(test_path) # Clean up except Exception as e: write_test["success"] = False write_test["error"] = str(e) return { "status": "ok", - "storage_info": storage_info, - "disk_usage": disk_usage, - "connection_info": connection_info, - "table_info": table_info, - "db_file_info": db_file_info, - "db_config": db_config, + "timestamp": datetime.utcnow().isoformat(), + "file_info": file_info, + "sqlite_test": sqlite_test, + "sqlalchemy_test": sqlalchemy_test, + "environment": env_info, "write_test": write_test } + except Exception as e: + # Catch-all error handler return { "status": "error", - "global_error": str(e), + "message": str(e), "traceback": traceback.format_exc() } \ No newline at end of file