Spaces:
Running
Running
Debug - User Add
Browse files- app/database.py +78 -29
app/database.py
CHANGED
@@ -3,22 +3,42 @@ import os
|
|
3 |
from databases import Database
|
4 |
from dotenv import load_dotenv
|
5 |
from sqlalchemy import create_engine, MetaData, Table, Column, Integer, String, text
|
6 |
-
import logging
|
|
|
7 |
|
8 |
load_dotenv()
|
9 |
-
logger = logging.getLogger(__name__)
|
10 |
|
11 |
-
# ---
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
|
16 |
-
#
|
17 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
18 |
|
19 |
-
database = Database(DATABASE_URL, connect_args=connect_args)
|
20 |
-
metadata = MetaData()
|
21 |
|
|
|
|
|
|
|
|
|
|
|
22 |
users = Table(
|
23 |
"users",
|
24 |
metadata,
|
@@ -27,52 +47,81 @@ users = Table(
|
|
27 |
Column("hashed_password", String, nullable=False),
|
28 |
)
|
29 |
|
30 |
-
#
|
31 |
-
# Derive the
|
32 |
-
sync_db_url =
|
|
|
|
|
|
|
|
|
|
|
33 |
logger.info(f"Using synchronous DB URL for initial check/create: {sync_db_url}")
|
34 |
-
engine
|
|
|
35 |
|
36 |
-
#
|
37 |
-
|
38 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
39 |
db_dir = os.path.dirname(db_file_path)
|
40 |
logger.info(f"Ensuring database directory exists: {db_dir}")
|
41 |
try:
|
42 |
if db_dir and not os.path.exists(db_dir):
|
43 |
os.makedirs(db_dir, exist_ok=True)
|
44 |
logger.info(f"Created database directory: {db_dir}")
|
|
|
|
|
|
|
|
|
|
|
|
|
45 |
except OSError as e:
|
46 |
-
logger.error(f"Error creating database directory {db_dir}: {e}")
|
47 |
-
|
|
|
|
|
48 |
|
49 |
-
# Now try connecting and creating the table
|
50 |
try:
|
51 |
logger.info("Attempting to connect with sync engine to check/create table...")
|
52 |
with engine.connect() as connection:
|
53 |
-
# Try a simple query to see if the table exists
|
54 |
try:
|
|
|
55 |
connection.execute(text("SELECT 1 FROM users LIMIT 1"))
|
56 |
logger.info("Users table already exists.")
|
57 |
-
except Exception: # Catch specific DB
|
58 |
-
logger.
|
59 |
-
|
|
|
60 |
logger.info("Users table created (or creation attempted).")
|
61 |
|
62 |
except Exception as e:
|
|
|
|
|
63 |
logger.exception(f"CRITICAL: Failed to connect/create database tables using sync engine: {e}")
|
64 |
-
# Application might fail to start properly here. Depending on requirements,
|
65 |
-
# you might raise the exception or just log it and hope the async part works.
|
66 |
-
# For now, just log it, as the async connection might still succeed later.
|
67 |
|
68 |
|
69 |
-
# Async connect/disconnect functions
|
70 |
async def connect_db():
|
71 |
try:
|
|
|
72 |
await database.connect()
|
73 |
-
logger.info(f"Database connection established (async): {
|
74 |
except Exception as e:
|
75 |
logger.exception(f"Failed to establish async database connection: {e}")
|
|
|
76 |
raise # Reraise critical error during startup lifespan
|
77 |
|
78 |
async def disconnect_db():
|
|
|
3 |
from databases import Database
|
4 |
from dotenv import load_dotenv
|
5 |
from sqlalchemy import create_engine, MetaData, Table, Column, Integer, String, text
|
6 |
+
import logging
|
7 |
+
from urllib.parse import urlparse, urlunparse, parse_qs, urlencode # For URL manipulation
|
8 |
|
9 |
load_dotenv()
|
10 |
+
logger = logging.getLogger(__name__)
|
11 |
|
12 |
+
# --- Database URL Configuration ---
|
13 |
+
DEFAULT_DB_PATH = "/data/app.db"
|
14 |
+
# Start with the base URL from env or default
|
15 |
+
raw_db_url = os.getenv("DATABASE_URL", f"sqlite+aiosqlite:///{DEFAULT_DB_PATH}")
|
16 |
|
17 |
+
# Ensure 'check_same_thread=False' is in the URL for SQLite async connection
|
18 |
+
final_database_url = raw_db_url
|
19 |
+
if raw_db_url.startswith("sqlite+aiosqlite"):
|
20 |
+
# Parse the URL
|
21 |
+
parsed_url = urlparse(raw_db_url)
|
22 |
+
# Parse existing query parameters into a dictionary
|
23 |
+
query_params = parse_qs(parsed_url.query)
|
24 |
+
# Add check_same_thread=False ONLY if it's not already there
|
25 |
+
# (in case it's set via DATABASE_URL env var)
|
26 |
+
if 'check_same_thread' not in query_params:
|
27 |
+
query_params['check_same_thread'] = ['False'] # Needs to be a list for urlencode
|
28 |
+
# Rebuild the query string
|
29 |
+
new_query = urlencode(query_params, doseq=True)
|
30 |
+
# Rebuild the URL using _replace method of the named tuple
|
31 |
+
final_database_url = urlunparse(parsed_url._replace(query=new_query))
|
32 |
+
logger.info(f"Using final async DB URL: {final_database_url}")
|
33 |
+
else:
|
34 |
+
logger.info(f"Using non-SQLite async DB URL: {final_database_url}")
|
35 |
|
|
|
|
|
36 |
|
37 |
+
# --- Async Database Instance (using 'databases' library) ---
|
38 |
+
# Pass the *modified* URL. DO NOT pass connect_args separately here.
|
39 |
+
database = Database(final_database_url)
|
40 |
+
|
41 |
+
metadata = MetaData()
|
42 |
users = Table(
|
43 |
"users",
|
44 |
metadata,
|
|
|
47 |
Column("hashed_password", String, nullable=False),
|
48 |
)
|
49 |
|
50 |
+
# --- Synchronous Engine for Initial Table Creation (using SQLAlchemy Core) ---
|
51 |
+
# Derive the sync URL (remove +aiosqlite). The query param should remain.
|
52 |
+
sync_db_url = final_database_url.replace("+aiosqlite", "")
|
53 |
+
|
54 |
+
# SQLAlchemy's create_engine *can* take connect_args, but for check_same_thread,
|
55 |
+
# it also understands it from the URL query string. Let's rely on the URL for simplicity.
|
56 |
+
# sync_connect_args = {"check_same_thread": False} if sync_db_url.startswith("sqlite") else {} # Keep for reference if other args are needed
|
57 |
+
|
58 |
logger.info(f"Using synchronous DB URL for initial check/create: {sync_db_url}")
|
59 |
+
# Create the engine using the URL which now includes ?check_same_thread=False
|
60 |
+
engine = create_engine(sync_db_url) # No connect_args needed here if only using check_same_thread
|
61 |
|
62 |
+
# --- Directory and Table Creation Logic ---
|
63 |
+
# Extract path correctly, ignoring query parameters for os.path operations
|
64 |
+
db_file_path = ""
|
65 |
+
if sync_db_url.startswith("sqlite"):
|
66 |
+
# Get the path part after 'sqlite:///' and before '?'
|
67 |
+
path_part = sync_db_url.split("sqlite:///")[-1].split("?")[0]
|
68 |
+
# Ensure it's an absolute path if it starts with /
|
69 |
+
if path_part.startswith('/'):
|
70 |
+
db_file_path = path_part
|
71 |
+
else:
|
72 |
+
# Handle relative paths if they were somehow configured (though /data should be absolute)
|
73 |
+
# This case is less likely with our default /data/app.db
|
74 |
+
db_file_path = os.path.abspath(path_part)
|
75 |
+
|
76 |
+
|
77 |
+
if db_file_path:
|
78 |
db_dir = os.path.dirname(db_file_path)
|
79 |
logger.info(f"Ensuring database directory exists: {db_dir}")
|
80 |
try:
|
81 |
if db_dir and not os.path.exists(db_dir):
|
82 |
os.makedirs(db_dir, exist_ok=True)
|
83 |
logger.info(f"Created database directory: {db_dir}")
|
84 |
+
# Add a check for writability after ensuring directory exists
|
85 |
+
if db_dir and not os.access(db_dir, os.W_OK):
|
86 |
+
logger.error(f"Database directory {db_dir} is not writable!")
|
87 |
+
# Also check if the file itself can be created/opened (might fail here if dir is writable but file isn't)
|
88 |
+
# This check is implicitly done by engine.connect() below
|
89 |
+
|
90 |
except OSError as e:
|
91 |
+
logger.error(f"Error creating or accessing database directory {db_dir}: {e}")
|
92 |
+
except Exception as e:
|
93 |
+
logger.error(f"Unexpected error checking/creating DB directory {db_dir}: {e}")
|
94 |
+
|
95 |
|
96 |
+
# Now try connecting and creating the table with the sync engine
|
97 |
try:
|
98 |
logger.info("Attempting to connect with sync engine to check/create table...")
|
99 |
with engine.connect() as connection:
|
|
|
100 |
try:
|
101 |
+
# Use text() for literal SQL
|
102 |
connection.execute(text("SELECT 1 FROM users LIMIT 1"))
|
103 |
logger.info("Users table already exists.")
|
104 |
+
except Exception as table_check_exc: # Catch specific DB errors if possible
|
105 |
+
logger.warning(f"Users table check failed ({type(table_check_exc).__name__}), attempting creation...")
|
106 |
+
# Pass the engine explicitly to create_all
|
107 |
+
metadata.create_all(bind=engine)
|
108 |
logger.info("Users table created (or creation attempted).")
|
109 |
|
110 |
except Exception as e:
|
111 |
+
# This OperationalError "unable to open database file" might still indicate
|
112 |
+
# a fundamental permission issue with /data/app.db in the HF environment.
|
113 |
logger.exception(f"CRITICAL: Failed to connect/create database tables using sync engine: {e}")
|
|
|
|
|
|
|
114 |
|
115 |
|
116 |
+
# --- Async connect/disconnect functions ---
|
117 |
async def connect_db():
|
118 |
try:
|
119 |
+
# The 'database' instance now uses the URL with the query param
|
120 |
await database.connect()
|
121 |
+
logger.info(f"Database connection established (async): {final_database_url}")
|
122 |
except Exception as e:
|
123 |
logger.exception(f"Failed to establish async database connection: {e}")
|
124 |
+
# If the sync engine failed earlier due to permissions, this might fail too.
|
125 |
raise # Reraise critical error during startup lifespan
|
126 |
|
127 |
async def disconnect_db():
|