PromptArmor / proxy /db_utils.py
Shourya Kakkar
Add robust retry loop for SQLite concurrent startup
f060971
Raw
History Blame Contribute Delete
5.15 kB
import os
import sqlite3
import pandas as pd
DATABASE_URL = os.getenv("DATABASE_URL", "")
DATABASE_PATH = os.getenv("DATABASE_PATH", "./proxy.db")
class DBConnectionWrapper:
def __init__(self, conn, is_pg=False):
self.conn = conn
self.is_pg = is_pg
def execute(self, query, params=()):
if self.is_pg:
query = query.replace("?", "%s")
cursor = self.conn.cursor()
cursor.execute(query, params)
return cursor
def commit(self):
self.conn.commit()
def close(self):
self.conn.close()
def get_db_connection():
if DATABASE_URL.startswith("postgres"):
import psycopg2
import psycopg2.extras
conn = psycopg2.connect(DATABASE_URL, cursor_factory=psycopg2.extras.DictCursor)
return DBConnectionWrapper(conn, is_pg=True)
else:
db_dir = os.path.dirname(DATABASE_PATH)
if db_dir:
os.makedirs(db_dir, exist_ok=True)
# Add a timeout to handle concurrent initialization locks
conn = sqlite3.connect(DATABASE_PATH, check_same_thread=False, timeout=15.0)
conn.row_factory = sqlite3.Row
return conn
def init_db() -> None:
"""Create the required database tables if they don't exist."""
import time
import sqlite3
for attempt in range(5):
try:
conn = get_db_connection()
try:
is_pg = DATABASE_URL.startswith("postgres")
id_type = "TEXT PRIMARY KEY"
# Users table
conn.execute(
"""
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
created_at TEXT NOT NULL
)
"""
)
# API Keys table
conn.execute(
"""
CREATE TABLE IF NOT EXISTS api_keys (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
key_hash TEXT NOT NULL,
prefix TEXT NOT NULL,
name TEXT,
created_at TEXT NOT NULL,
FOREIGN KEY(user_id) REFERENCES users(id)
)
"""
)
# Requests table
conn.execute(
f"""
CREATE TABLE IF NOT EXISTS requests (
id {id_type},
timestamp TEXT NOT NULL,
user_id TEXT,
prompt_text TEXT NOT NULL,
system_prompt TEXT,
score_heuristic REAL,
score_classifier REAL,
score_embedding REAL,
score_judge REAL,
final_score REAL NOT NULL,
action_taken TEXT NOT NULL,
triggered_layers TEXT NOT NULL,
matched_patterns TEXT,
judge_reason TEXT,
model TEXT,
processing_ms REAL,
FOREIGN KEY(user_id) REFERENCES users(id)
)
"""
)
if is_pg:
try:
conn.execute("ALTER TABLE requests ALTER COLUMN id TYPE TEXT")
except Exception:
pass
if not is_pg:
try:
conn.execute("ALTER TABLE requests ADD COLUMN user_id TEXT")
except Exception:
pass
conn.commit()
return # Success, exit the retry loop
finally:
conn.close()
except sqlite3.OperationalError as e:
if attempt < 4:
print(f"SQLite concurrent init error (attempt {attempt+1}/5): {e}")
time.sleep(1.5) # Wait for the other process to finish creating tables
else:
print(f"Failed to initialize database after 5 attempts: {e}")
raise
def query_db_df(query: str, params: tuple = (), conn=None) -> pd.DataFrame:
close_conn = False
if conn is None:
conn = get_db_connection()
close_conn = True
if DATABASE_URL.startswith("postgres"):
query = query.replace("?", "%s")
try:
# Pandas requires the raw DBAPI connection object, not our wrapper
raw_conn = conn.conn if hasattr(conn, "conn") else conn
return pd.read_sql_query(query, raw_conn, params=params)
except Exception as e:
print(f"DB Error: {e}")
return pd.DataFrame()
finally:
if close_conn:
conn.close()