goalsbreakdown/schema.py
Yuyao Huang 2a3482c2c0 fix: prevent UNIQUE constraint race with gunicorn multi-worker
Use INSERT OR IGNORE instead of SELECT-then-INSERT for
default admin user creation, avoiding the race condition
when multiple gunicorn workers initialize simultaneously.
2026-05-09 10:19:58 +08:00

100 lines
2.6 KiB
Python

import sqlite3
import os
import config
CREATE_USERS = """
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'user',
max_goals INTEGER NOT NULL DEFAULT 5,
selected_goal_id INTEGER
)
"""
CREATE_GOALS = """
CREATE TABLE IF NOT EXISTS goals (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
title TEXT NOT NULL,
activated INTEGER NOT NULL DEFAULT 1,
selected_task_id INTEGER,
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (selected_task_id) REFERENCES tasks(id)
)
"""
CREATE_TASKS = """
CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
goal_id INTEGER NOT NULL,
title TEXT NOT NULL,
desc TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'todo',
start_time TEXT,
finished_time TEXT,
"order" REAL NOT NULL DEFAULT 0.0,
FOREIGN KEY (goal_id) REFERENCES goals(id)
)
"""
CREATE_NOTES = """
CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
goal_id INTEGER,
task_id INTEGER,
title TEXT NOT NULL,
content TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (goal_id) REFERENCES goals(id) ON DELETE CASCADE,
FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE
)
"""
def get_connection():
os.makedirs(os.path.dirname(config.DB_PATH), exist_ok=True)
conn = sqlite3.connect(config.DB_PATH)
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA foreign_keys = ON")
return conn
def init_db():
conn = get_connection()
try:
conn.execute(CREATE_USERS)
conn.execute(CREATE_GOALS)
conn.execute(CREATE_TASKS)
conn.execute(CREATE_NOTES)
try:
conn.execute("ALTER TABLE goals ADD COLUMN selected_task_id INTEGER")
except sqlite3.OperationalError:
pass
try:
conn.execute("ALTER TABLE users ADD COLUMN selected_goal_id INTEGER")
except sqlite3.OperationalError:
pass
conn.commit()
import bcrypt
password_hash = bcrypt.hashpw(
config.DEFAULT_ADMIN_PASSWORD.encode("utf-8"),
bcrypt.gensalt()
).decode("utf-8")
conn.execute(
"INSERT OR IGNORE INTO users (username, password_hash, role, max_goals) VALUES (?, ?, ?, ?)",
(config.DEFAULT_ADMIN_USERNAME, password_hash, "admin", 100)
)
conn.commit()
finally:
conn.close()