| """ |
| SQLite Database Module |
| βββββββββββββββββββββ |
| Handles connection, initialization, and health checks. |
| |
| Sources: |
| - Python sqlite3 docs: https://docs.python.org/3/library/sqlite3.html |
| - Litestream tips: https://litestream.io/tips/ |
| - SQLite WAL mode: https://www.sqlite.org/wal.html |
| - SQLite PRAGMA: https://www.sqlite.org/pragma.html |
| """ |
|
|
| import sqlite3 |
| import os |
| import threading |
| import time |
|
|
| DB_PATH = "/tmp/data/app.db" |
|
|
| |
| _local = threading.local() |
|
|
|
|
| def get_db() -> sqlite3.Connection: |
| """ |
| Get a thread-safe database connection. |
| |
| Each thread gets its own connection (SQLite requirement for WAL mode). |
| Connections are cached per-thread to avoid overhead. |
| |
| PRAGMA explanations: |
| - journal_mode=WAL β Required for Litestream. Allows concurrent reads. |
| - busy_timeout=5000 β Wait 5s if another thread is writing. |
| - synchronous=NORMAL β Safe with WAL, faster than FULL. |
| - cache_size=-20000 β 20MB page cache in memory. |
| - foreign_keys=ON β Enforce relational integrity. |
| - wal_autocheckpoint=1000 β Checkpoint every 1000 pages (~4MB). |
| """ |
| if not hasattr(_local, 'conn') or _local.conn is None: |
| os.makedirs(os.path.dirname(DB_PATH), exist_ok=True) |
| |
| conn = sqlite3.connect(DB_PATH, check_same_thread=False) |
| conn.row_factory = sqlite3.Row |
| |
| |
| conn.execute("PRAGMA journal_mode=WAL") |
| conn.execute("PRAGMA busy_timeout=5000") |
| conn.execute("PRAGMA synchronous=NORMAL") |
| conn.execute("PRAGMA cache_size=-20000") |
| conn.execute("PRAGMA foreign_keys=ON") |
| conn.execute("PRAGMA wal_autocheckpoint=1000") |
| |
| _local.conn = conn |
| |
| return _local.conn |
|
|
|
|
| def close_db(): |
| """Close the thread-local connection.""" |
| if hasattr(_local, 'conn') and _local.conn: |
| _local.conn.close() |
| _local.conn = None |
|
|
|
|
| def init_db(): |
| """ |
| Create tables on first run. |
| Safe to call multiple times (IF NOT EXISTS). |
| """ |
| conn = get_db() |
| conn.executescript(""" |
| -- βββββββββββββββββββββββββββββββββββββββββββ |
| -- USERS |
| -- βββββββββββββββββββββββββββββββββββββββββββ |
| CREATE TABLE IF NOT EXISTS users ( |
| id TEXT PRIMARY KEY, |
| email TEXT UNIQUE NOT NULL, |
| name TEXT, |
| avatar_url TEXT, |
| created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, |
| updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP |
| ); |
| |
| -- βββββββββββββββββββββββββββββββββββββββββββ |
| -- PROJECTS |
| -- βββββββββββββββββββββββββββββββββββββββββββ |
| CREATE TABLE IF NOT EXISTS projects ( |
| id TEXT PRIMARY KEY, |
| user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, |
| name TEXT NOT NULL, |
| description TEXT, |
| design_tokens JSON, |
| status TEXT DEFAULT 'draft' CHECK(status IN ('draft','active','archived')), |
| created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, |
| updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP |
| ); |
| |
| -- βββββββββββββββββββββββββββββββββββββββββββ |
| -- COLOR PALETTES |
| -- βββββββββββββββββββββββββββββββββββββββββββ |
| CREATE TABLE IF NOT EXISTS color_palettes ( |
| id TEXT PRIMARY KEY, |
| project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE, |
| name TEXT NOT NULL, |
| colors JSON NOT NULL, |
| mood TEXT, |
| created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP |
| ); |
| |
| -- βββββββββββββββββββββββββββββββββββββββββββ |
| -- DESIGN ASSETS (references to HF Bucket files) |
| -- βββββββββββββββββββββββββββββββββββββββββββ |
| CREATE TABLE IF NOT EXISTS design_assets ( |
| id TEXT PRIMARY KEY, |
| project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE, |
| asset_type TEXT NOT NULL, |
| bucket_path TEXT NOT NULL, |
| file_size INTEGER, |
| metadata JSON, |
| created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP |
| ); |
| |
| -- βββββββββββββββββββββββββββββββββββββββββββ |
| -- BACKUP LOG (tracks backup history) |
| -- βββββββββββββββββββββββββββββββββββββββββββ |
| CREATE TABLE IF NOT EXISTS backup_log ( |
| id INTEGER PRIMARY KEY AUTOINCREMENT, |
| backup_type TEXT NOT NULL, |
| file_path TEXT, |
| file_size INTEGER, |
| status TEXT DEFAULT 'success', |
| error_message TEXT, |
| created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP |
| ); |
| |
| -- βββββββββββββββββββββββββββββββββββββββββββ |
| -- INDEXES |
| -- βββββββββββββββββββββββββββββββββββββββββββ |
| CREATE INDEX IF NOT EXISTS idx_projects_user |
| ON projects(user_id); |
| CREATE INDEX IF NOT EXISTS idx_palettes_project |
| ON color_palettes(project_id); |
| CREATE INDEX IF NOT EXISTS idx_assets_project |
| ON design_assets(project_id); |
| CREATE INDEX IF NOT EXISTS idx_backup_log_date |
| ON backup_log(created_at); |
| """) |
| conn.commit() |
|
|
|
|
| def integrity_check() -> dict: |
| """ |
| Run SQLite integrity check. |
| Returns health status of the database. |
| |
| Source: https://www.sqlite.org/pragma.html#pragma_integrity_check |
| """ |
| conn = get_db() |
| result = conn.execute("PRAGMA integrity_check").fetchone() |
| wal_size = conn.execute("PRAGMA wal_checkpoint(PASSIVE)").fetchone() |
| page_count = conn.execute("PRAGMA page_count").fetchone()[0] |
| page_size = conn.execute("PRAGMA page_size").fetchone()[0] |
| |
| db_size = page_count * page_size |
| |
| return { |
| "integrity": result[0], |
| "db_size_bytes": db_size, |
| "db_size_mb": round(db_size / (1024 * 1024), 2), |
| "page_count": page_count, |
| "page_size": page_size, |
| "wal_pages": wal_size[1] if wal_size else 0, |
| "journal_mode": conn.execute("PRAGMA journal_mode").fetchone()[0], |
| } |
|
|