Initial commit

This commit is contained in:
Peter Stockings
2026-02-22 22:53:22 +11:00
commit ccdb3d8dc7
26 changed files with 2238 additions and 0 deletions

View File

@@ -0,0 +1,67 @@
-- Migration 001: Initial Schema
-- Creates the core tables for the weight tracker app
-- Users table
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
display_name VARCHAR(100),
height_cm DECIMAL(5,1),
age INTEGER,
gender VARCHAR(20),
goal_weight_kg DECIMAL(5,1),
starting_weight_kg DECIMAL(5,1),
created_at TIMESTAMP DEFAULT NOW()
);
-- Check-ins table
CREATE TABLE IF NOT EXISTS checkins (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
weight_kg DECIMAL(5,1) NOT NULL,
bmi DECIMAL(4,1),
notes TEXT,
checked_in_at TIMESTAMP DEFAULT NOW()
);
-- Milestones table
CREATE TABLE IF NOT EXISTS milestones (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
milestone_key VARCHAR(50) NOT NULL,
achieved_at TIMESTAMP DEFAULT NOW(),
UNIQUE(user_id, milestone_key)
);
-- Challenges table
CREATE TABLE IF NOT EXISTS challenges (
id SERIAL PRIMARY KEY,
title VARCHAR(200) NOT NULL,
description TEXT,
target_type VARCHAR(30) NOT NULL,
target_value DECIMAL(8,2) NOT NULL,
start_date DATE NOT NULL,
end_date DATE NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
-- Challenge participants
CREATE TABLE IF NOT EXISTS challenge_participants (
id SERIAL PRIMARY KEY,
challenge_id INTEGER REFERENCES challenges(id) ON DELETE CASCADE,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
completed BOOLEAN DEFAULT FALSE,
completed_at TIMESTAMP,
UNIQUE(challenge_id, user_id)
);
-- Indexes
CREATE INDEX IF NOT EXISTS idx_checkins_user_date ON checkins(user_id, checked_in_at DESC);
CREATE INDEX IF NOT EXISTS idx_milestones_user ON milestones(user_id);
-- Migrations tracking table
CREATE TABLE IF NOT EXISTS schema_migrations (
version INTEGER PRIMARY KEY,
applied_at TIMESTAMP DEFAULT NOW()
);

106
migrations/runner.py Normal file
View File

@@ -0,0 +1,106 @@
"""
Database Migration Runner
Reads SQL migration files from the migrations/ directory, checks which
have already been applied via the schema_migrations table, and runs
any unapplied migrations in order.
Usage:
python migrations/runner.py
"""
import os
import re
import sys
import psycopg2
from dotenv import load_dotenv
def get_migration_files(migrations_dir):
"""Find all SQL migration files and return sorted by version number."""
pattern = re.compile(r"^(\d+)_.+\.sql$")
files = []
for filename in os.listdir(migrations_dir):
match = pattern.match(filename)
if match:
version = int(match.group(1))
files.append((version, filename))
return sorted(files, key=lambda x: x[0])
def ensure_migrations_table(conn):
"""Create schema_migrations table if it doesn't exist."""
with conn.cursor() as cur:
cur.execute("""
CREATE TABLE IF NOT EXISTS schema_migrations (
version INTEGER PRIMARY KEY,
applied_at TIMESTAMP DEFAULT NOW()
)
""")
conn.commit()
def get_applied_versions(conn):
"""Get set of already-applied migration versions."""
with conn.cursor() as cur:
cur.execute("SELECT version FROM schema_migrations ORDER BY version")
return {row[0] for row in cur.fetchall()}
def run_migration(conn, version, filepath):
"""Run a single migration file inside a transaction."""
print(f" Applying migration {version}: {os.path.basename(filepath)}...")
with open(filepath, "r", encoding="utf-8") as f:
sql = f.read()
with conn.cursor() as cur:
cur.execute(sql)
cur.execute(
"INSERT INTO schema_migrations (version) VALUES (%s)",
(version,),
)
conn.commit()
print(f" ✓ Migration {version} applied successfully")
def main():
load_dotenv()
database_url = os.environ.get("DATABASE_URL")
if not database_url:
print("ERROR: DATABASE_URL not set in .env")
sys.exit(1)
# Determine migrations directory
migrations_dir = os.path.dirname(os.path.abspath(__file__))
print(f"Connecting to database...")
conn = psycopg2.connect(database_url)
try:
ensure_migrations_table(conn)
applied = get_applied_versions(conn)
migrations = get_migration_files(migrations_dir)
pending = [(v, f) for v, f in migrations if v not in applied]
if not pending:
print("All migrations are up to date.")
return
print(f"Found {len(pending)} pending migration(s):")
for version, filename in pending:
filepath = os.path.join(migrations_dir, filename)
run_migration(conn, version, filepath)
print("\nAll migrations applied successfully!")
except Exception as e:
conn.rollback()
print(f"\nERROR: Migration failed: {e}")
sys.exit(1)
finally:
conn.close()
if __name__ == "__main__":
main()