algospaced-dsa / sandbox_db.py
iamfebin's picture
feat: implement sandbox mode
a95fdd1
Raw
History Blame Contribute Delete
21.4 kB
import sqlite3
import uuid
import os
from datetime import datetime, timezone
DB_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__)), "scratch")
DB_PATH = os.path.join(DB_FOLDER, "sandbox.db")
def init_db():
os.makedirs(DB_FOLDER, exist_ok=True)
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
# Enable foreign keys
cursor.execute("PRAGMA foreign_keys = ON;")
# 1. Problems table
cursor.execute("""
CREATE TABLE IF NOT EXISTS problems (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
name TEXT NOT NULL,
problem_number INTEGER,
pattern TEXT NOT NULL,
difficulty TEXT NOT NULL,
google_doc_url TEXT,
google_doc_id TEXT,
optimal_time TEXT DEFAULT 'O(N)',
optimal_space TEXT DEFAULT 'O(N)',
description TEXT,
reference_code TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_id, name)
);
""")
# 2. User Reviews table
cursor.execute("""
CREATE TABLE IF NOT EXISTS user_reviews (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
problem_id TEXT NOT NULL,
box_level INTEGER DEFAULT 1,
last_reviewed TEXT DEFAULT CURRENT_TIMESTAMP,
next_review TEXT DEFAULT CURRENT_TIMESTAMP,
times_correct INTEGER DEFAULT 0,
total_attempts INTEGER DEFAULT 0,
UNIQUE(user_id, problem_id),
FOREIGN KEY(problem_id) REFERENCES problems(id) ON DELETE CASCADE
);
""")
# 3. User Streaks table
cursor.execute("""
CREATE TABLE IF NOT EXISTS user_streaks (
user_id TEXT PRIMARY KEY,
current_streak INTEGER DEFAULT 0,
longest_streak INTEGER DEFAULT 0,
last_active_date TEXT,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
);
""")
# 4. User Journey Progress table
cursor.execute("""
CREATE TABLE IF NOT EXISTS user_journey_progress (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
problem_number INTEGER NOT NULL,
viewed_at TEXT DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_id, problem_number)
);
""")
# 5. User Scores table
cursor.execute("""
CREATE TABLE IF NOT EXISTS user_scores (
user_id TEXT PRIMARY KEY,
total_score INTEGER DEFAULT 0,
python_challenges_completed INTEGER DEFAULT 0,
sql_challenges_completed INTEGER DEFAULT 0,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
);
""")
# 6. Challenge History table
cursor.execute("""
CREATE TABLE IF NOT EXISTS challenge_history (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
challenge_type TEXT NOT NULL,
challenge_title TEXT NOT NULL,
points_awarded INTEGER DEFAULT 0,
completed_at TEXT DEFAULT CURRENT_TIMESTAMP
);
""")
conn.commit()
# Seed default problems and reviews if empty
cursor.execute("SELECT COUNT(*) FROM problems WHERE user_id = 'sandbox-user-id'")
if cursor.fetchone()[0] == 0:
seed_default_problems(cursor)
conn.commit()
conn.close()
def seed_default_problems(cursor):
default_problems = [
{
"id": "prob-contains-duplicate",
"user_id": "sandbox-user-id",
"name": "Contains Duplicate",
"problem_number": 217,
"pattern": "Arrays & Hashing",
"difficulty": "Easy",
"google_doc_url": "https://leetcode.com/problems/contains-duplicate/",
"google_doc_id": "doc-contains-duplicate",
"optimal_time": "O(N)",
"optimal_space": "O(N)",
"description": "Given an integer array nums, return true if any value appears at least twice in the array, and return false if every element is distinct.",
"reference_code": "def solve(nums: list[int]) -> bool:\n # Return True if any value appears at least twice\n return len(nums) != len(set(nums))"
},
{
"id": "prob-valid-anagram",
"user_id": "sandbox-user-id",
"name": "Valid Anagram",
"problem_number": 242,
"pattern": "Arrays & Hashing",
"difficulty": "Easy",
"google_doc_url": "https://leetcode.com/problems/valid-anagram/",
"google_doc_id": "doc-valid-anagram",
"optimal_time": "O(N)",
"optimal_space": "O(1)",
"description": "Given two strings s and t, return true if t is an anagram of s, and false otherwise.",
"reference_code": "def solve(s: str, t: str) -> bool:\n # Return True if t is an anagram of s\n if len(s) != len(t):\n return False\n return sorted(s) == sorted(t)"
},
{
"id": "prob-two-sum",
"user_id": "sandbox-user-id",
"name": "Two Sum",
"problem_number": 1,
"pattern": "Arrays & Hashing",
"difficulty": "Easy",
"google_doc_url": "https://leetcode.com/problems/two-sum/",
"google_doc_id": "doc-two-sum",
"optimal_time": "O(N)",
"optimal_space": "O(N)",
"description": "Given an array of integers nums and an integer target, return indices of the two numbers such that they add up to target.",
"reference_code": "def solve(nums: list[int], target: int) -> list[int]:\n # Return indices of the two numbers that add up to target\n seen = {}\n for i, n in enumerate(nums):\n diff = target - n\n if diff in seen:\n return [seen[diff], i]\n seen[n] = i\n return []"
},
{
"id": "prob-group-anagrams",
"user_id": "sandbox-user-id",
"name": "Group Anagrams",
"problem_number": 49,
"pattern": "Arrays & Hashing",
"difficulty": "Medium",
"google_doc_url": "https://leetcode.com/problems/group-anagrams/",
"google_doc_id": "doc-group-anagrams",
"optimal_time": "O(N * K log K)",
"optimal_space": "O(N * K)",
"description": "Given an array of strings strs, group the anagrams together. You can return the answer in any order.",
"reference_code": "def solve(strs: list[str]) -> list[list[str]]:\n # Group anagrams together\n ans = {}\n for s in strs:\n key = ''.join(sorted(s))\n if key not in ans:\n ans[key] = []\n ans[key].append(s)\n return list(ans.values())"
},
{
"id": "prob-valid-parentheses",
"user_id": "sandbox-user-id",
"name": "Valid Parentheses",
"problem_number": 20,
"pattern": "Stack",
"difficulty": "Easy",
"google_doc_url": "https://leetcode.com/problems/valid-parentheses/",
"google_doc_id": "doc-valid-parentheses",
"optimal_time": "O(N)",
"optimal_space": "O(N)",
"description": "Given a string s containing just the characters '(', ')', '{', '}', '[' and ']', determine if the input string is valid.",
"reference_code": "def solve(s: str) -> bool:\n # Determine if the parentheses input string is valid\n stack = []\n mapping = {')': '(', '}': '{', ']': '['}\n for char in s:\n if char in mapping:\n top_element = stack.pop() if stack else '#'\n if mapping[char] != top_element:\n return False\n else:\n stack.append(char)\n return not stack"
}
]
for p in default_problems:
columns = list(p.keys())
placeholders = ", ".join(["?"] * len(columns))
query = f"INSERT OR IGNORE INTO problems ({', '.join(columns)}) VALUES ({placeholders})"
cursor.execute(query, list(p.values()))
now_iso = datetime.now(timezone.utc).isoformat()
default_reviews = [
{
"id": "rev-contains-duplicate",
"user_id": "sandbox-user-id",
"problem_id": "prob-contains-duplicate",
"box_level": 1,
"last_reviewed": now_iso,
"next_review": now_iso,
"times_correct": 0,
"total_attempts": 0
},
{
"id": "rev-valid-anagram",
"user_id": "sandbox-user-id",
"problem_id": "prob-valid-anagram",
"box_level": 1,
"last_reviewed": now_iso,
"next_review": now_iso,
"times_correct": 0,
"total_attempts": 0
},
{
"id": "rev-two-sum",
"user_id": "sandbox-user-id",
"problem_id": "prob-two-sum",
"box_level": 1,
"last_reviewed": now_iso,
"next_review": now_iso,
"times_correct": 0,
"total_attempts": 0
}
]
for r in default_reviews:
cols = list(r.keys())
places = ", ".join(["?"] * len(cols))
query = f"INSERT OR IGNORE INTO user_reviews ({', '.join(cols)}) VALUES ({places})"
cursor.execute(query, list(r.values()))
class MockResponse:
def __init__(self, data):
self.data = data
class QueryBuilder:
def __init__(self, table_name, real_client=None):
self.table_name = table_name
self.real_client = real_client
self.filters = []
self.order_by = []
self.limit_val = None
self.op = None
self.op_data = None
self.on_conflict = None
def select(self, columns="*"):
self.op = 'select'
self.op_data = columns
return self
def insert(self, data):
self.op = 'insert'
self.op_data = data
return self
def update(self, data):
self.op = 'update'
self.op_data = data
return self
def upsert(self, data, on_conflict=None):
self.op = 'upsert'
self.op_data = data
self.on_conflict = on_conflict
return self
def delete(self):
self.op = 'delete'
return self
def eq(self, column, value):
self.filters.append(('eq', column, value))
return self
def lte(self, column, value):
self.filters.append(('lte', column, value))
return self
def order(self, column, desc=False):
self.order_by.append((column, desc))
return self
def limit(self, count):
self.limit_val = count
return self
def execute(self):
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
try:
if self.op == 'select':
# Build SELECT query
# If table is 'problems' and real client is online, try fetching from Supabase first
if self.table_name == 'problems' and self.real_client:
try:
# Build public Supabase query
supabase_query = self.real_client.table('problems').select('*')
for op, col, val in self.filters:
if op == 'eq':
supabase_query = supabase_query.eq(col, val)
elif op == 'lte':
supabase_query = supabase_query.lte(col, val)
for col, desc in self.order_by:
supabase_query = supabase_query.order(col, desc=desc)
if self.limit_val is not None:
supabase_query = supabase_query.limit(self.limit_val)
res = supabase_query.execute()
data = res.data if res.data else []
# Post-process for nested table relations problems -> user_reviews
if self.op_data and 'user_reviews(' in self.op_data:
for row in data:
prob_id = row.get('id')
if prob_id:
cursor.execute("SELECT * FROM user_reviews WHERE problem_id = ? AND user_id = 'sandbox-user-id'", (prob_id,))
rev_rows = cursor.fetchall()
row['user_reviews'] = [dict(r) for r in rev_rows]
return MockResponse(data)
except Exception as e:
# Fallback silently to SQLite
pass
query = f"SELECT * FROM {self.table_name}"
params = []
where_clauses = []
for op, col, val in self.filters:
if op == 'eq':
where_clauses.append(f"{col} = ?")
params.append(val)
elif op == 'lte':
where_clauses.append(f"{col} <= ?")
params.append(val)
if where_clauses:
query += " WHERE " + " AND ".join(where_clauses)
# Order by
if self.order_by:
order_clauses = []
for col, desc in self.order_by:
direction = "DESC" if desc else "ASC"
order_clauses.append(f"{col} {direction}")
query += " ORDER BY " + ", ".join(order_clauses)
# Limit
if self.limit_val is not None:
query += f" LIMIT {self.limit_val}"
cursor.execute(query, params)
rows = cursor.fetchall()
data = [dict(row) for row in rows]
# Post-process for nested table relations
# 1. user_reviews -> problems
if self.table_name == 'user_reviews' and self.op_data and 'problems(*)' in self.op_data:
for row in data:
prob_id = row.get('problem_id')
if prob_id:
cursor.execute("SELECT * FROM problems WHERE id = ?", (prob_id,))
prob_row = cursor.fetchone()
if prob_row:
row['problems'] = dict(prob_row)
else:
# Fallback to real Supabase client for public problems
if self.real_client:
try:
res_p = self.real_client.table('problems').select('*').eq('id', prob_id).execute()
row['problems'] = res_p.data[0] if res_p.data else None
except Exception:
row['problems'] = None
else:
row['problems'] = None
# 2. problems -> user_reviews
elif self.table_name == 'problems' and self.op_data and 'user_reviews(' in self.op_data:
for row in data:
prob_id = row.get('id')
if prob_id:
cursor.execute("SELECT * FROM user_reviews WHERE problem_id = ? AND user_id = 'sandbox-user-id'", (prob_id,))
rev_rows = cursor.fetchall()
row['user_reviews'] = [dict(r) for r in rev_rows]
return MockResponse(data)
elif self.op == 'insert':
data_list = self.op_data if isinstance(self.op_data, list) else [self.op_data]
inserted_rows = []
for row_data in data_list:
# Auto generate UUID if not exists or None
if 'id' in row_data and row_data['id'] is None:
row_data['id'] = str(uuid.uuid4())
elif self.table_name in ('problems', 'user_reviews', 'user_journey_progress', 'challenge_history') and 'id' not in row_data:
row_data['id'] = str(uuid.uuid4())
columns = list(row_data.keys())
placeholders = ", ".join(["?"] * len(columns))
query = f"INSERT INTO {self.table_name} ({', '.join(columns)}) VALUES ({placeholders})"
cursor.execute(query, list(row_data.values()))
inserted_rows.append(row_data)
conn.commit()
return MockResponse(inserted_rows)
elif self.op == 'update':
columns = list(self.op_data.keys())
set_clause = ", ".join([f"{col} = ?" for col in columns])
params = list(self.op_data.values())
query = f"UPDATE {self.table_name} SET {set_clause}"
where_clauses = []
where_params = []
for op, col, val in self.filters:
if op == 'eq':
where_clauses.append(f"{col} = ?")
where_params.append(val)
elif op == 'lte':
where_clauses.append(f"{col} <= ?")
where_params.append(val)
if where_clauses:
query += " WHERE " + " AND ".join(where_clauses)
cursor.execute(query, params + where_params)
conn.commit()
# Fetch and return updated rows
select_query = f"SELECT * FROM {self.table_name}"
if where_clauses:
select_query += " WHERE " + " AND ".join(where_clauses)
cursor.execute(select_query, where_params)
rows = cursor.fetchall()
return MockResponse([dict(row) for row in rows])
elif self.op == 'upsert':
row_data = self.op_data if not isinstance(self.op_data, list) else self.op_data[0]
if self.table_name == 'user_journey_progress':
if 'id' not in row_data:
row_data['id'] = str(uuid.uuid4())
columns = list(row_data.keys())
placeholders = ", ".join(["?"] * len(columns))
query = f"""
INSERT INTO user_journey_progress ({', '.join(columns)})
VALUES ({placeholders})
ON CONFLICT(user_id, problem_number)
DO UPDATE SET viewed_at = excluded.viewed_at
"""
cursor.execute(query, list(row_data.values()))
else:
columns = list(row_data.keys())
placeholders = ", ".join(["?"] * len(columns))
query = f"INSERT OR REPLACE INTO {self.table_name} ({', '.join(columns)}) VALUES ({placeholders})"
cursor.execute(query, list(row_data.values()))
conn.commit()
return MockResponse([row_data])
elif self.op == 'delete':
query = f"DELETE FROM {self.table_name}"
params = []
where_clauses = []
for op, col, val in self.filters:
if op == 'eq':
where_clauses.append(f"{col} = ?")
params.append(val)
if where_clauses:
query += " WHERE " + " AND ".join(where_clauses)
cursor.execute(query, params)
conn.commit()
return MockResponse([])
finally:
conn.close()
class MockSupabaseClient:
def __init__(self):
self.current_user_id = 'sandbox-user-id'
# Ensure database and tables exist
init_db()
# Initialize a real Supabase client for reading public data
from supabase import create_client
import os
import toml
url = os.environ.get("SUPABASE_URL")
key = os.environ.get("SUPABASE_KEY")
if not url or not key:
try:
secrets_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), ".streamlit", "secrets.toml")
if os.path.exists(secrets_path):
secrets = toml.load(secrets_path)
url = url or secrets.get("SUPABASE_URL")
key = key or secrets.get("SUPABASE_KEY")
except Exception:
pass
self.real_client = None
if url and key:
try:
self.real_client = create_client(url, key)
except Exception as e:
print(f"Error creating real client in sandbox: {e}")
def table(self, table_name):
return QueryBuilder(table_name, real_client=self.real_client)
def get_sandbox_client():
return MockSupabaseClient()