from __future__ import annotations from datetime import date, datetime from typing import Any, Optional from sqlalchemy import Boolean, Date, DateTime, Float, ForeignKey, Integer, String, Text, UniqueConstraint from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.orm import Mapped, mapped_column, relationship from app.database import Base class User(Base): __tablename__ = "users" id: Mapped[int] = mapped_column(Integer, primary_key=True) password_hash: Mapped[str] = mapped_column(String(255), nullable=False) created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) class Profile(Base): __tablename__ = "profiles" id: Mapped[int] = mapped_column(Integer, primary_key=True) one_rm_kg: Mapped[Optional[float]] = mapped_column(Float, nullable=True) training_weekdays: Mapped[Optional[list[Any]]] = mapped_column(JSONB, nullable=True) start_date: Mapped[Optional[date]] = mapped_column(Date, nullable=True) onboarded: Mapped[bool] = mapped_column(Boolean, default=False) phase: Mapped[str] = mapped_column(String(32), default="active") # active | rest | probe | completed_cycle rest_until: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) probe_available_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) cycle_number: Mapped[int] = mapped_column(Integer, default=1) updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) class Workout(Base): __tablename__ = "workouts" __table_args__ = (UniqueConstraint("scheduled_date", "cycle_number", name="uq_workout_date_cycle"),) id: Mapped[int] = mapped_column(Integer, primary_key=True) cycle_number: Mapped[int] = mapped_column(Integer, default=1) week_number: Mapped[int] = mapped_column(Integer, nullable=False) day_index: Mapped[int] = mapped_column(Integer, nullable=False) # 0..2 within week scheduled_date: Mapped[date] = mapped_column(Date, nullable=False, index=True) status: Mapped[str] = mapped_column(String(32), default="planned") # planned | in_progress | paused | completed | abandoned started_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) paused_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) completed_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) current_exercise_index: Mapped[int] = mapped_column(Integer, default=0) current_set_index: Mapped[int] = mapped_column(Integer, default=0) rest_started_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) exercises: Mapped[list[WorkoutExercise]] = relationship( back_populates="workout", cascade="all, delete-orphan", order_by="WorkoutExercise.order_index" ) class WorkoutExercise(Base): __tablename__ = "workout_exercises" id: Mapped[int] = mapped_column(Integer, primary_key=True) workout_id: Mapped[int] = mapped_column(ForeignKey("workouts.id", ondelete="CASCADE"), index=True) order_index: Mapped[int] = mapped_column(Integer, nullable=False) name: Mapped[str] = mapped_column(String(128), nullable=False) kind: Mapped[str] = mapped_column(String(32), nullable=False) # bench | accessory | failure prescribed_weight_kg: Mapped[Optional[float]] = mapped_column(Float, nullable=True) weight_locked: Mapped[bool] = mapped_column(Boolean, default=False) needs_weight_input: Mapped[bool] = mapped_column(Boolean, default=False) pending_prompt: Mapped[Optional[str]] = mapped_column(String(32), nullable=True) # None | choose_weight_missed | choose_weight_easy | confirm_easy workout: Mapped[Workout] = relationship(back_populates="exercises") sets: Mapped[list[WorkoutSet]] = relationship( back_populates="exercise", cascade="all, delete-orphan", order_by="WorkoutSet.set_index" ) class WorkoutSet(Base): __tablename__ = "workout_sets" id: Mapped[int] = mapped_column(Integer, primary_key=True) exercise_id: Mapped[int] = mapped_column(ForeignKey("workout_exercises.id", ondelete="CASCADE"), index=True) set_index: Mapped[int] = mapped_column(Integer, nullable=False) target_percent: Mapped[Optional[float]] = mapped_column(Float, nullable=True) target_reps: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) target_weight_kg: Mapped[Optional[float]] = mapped_column(Float, nullable=True) is_failure: Mapped[bool] = mapped_column(Boolean, default=False) completed: Mapped[bool] = mapped_column(Boolean, default=False) actual_reps: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) actual_weight_kg: Mapped[Optional[float]] = mapped_column(Float, nullable=True) completed_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) exercise: Mapped[WorkoutExercise] = relationship(back_populates="sets") class AccessoryState(Base): """Tracks working weight and lock status for accessory exercises within a cycle.""" __tablename__ = "accessory_states" __table_args__ = (UniqueConstraint("cycle_number", "exercise_name", name="uq_accessory_cycle_name"),) id: Mapped[int] = mapped_column(Integer, primary_key=True) cycle_number: Mapped[int] = mapped_column(Integer, default=1) exercise_name: Mapped[str] = mapped_column(String(128), nullable=False) weight_kg: Mapped[Optional[float]] = mapped_column(Float, nullable=True) locked: Mapped[bool] = mapped_column(Boolean, default=False) pending_weight_kg: Mapped[Optional[float]] = mapped_column(Float, nullable=True) updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) class AppMeta(Base): __tablename__ = "app_meta" id: Mapped[int] = mapped_column(Integer, primary_key=True) key: Mapped[str] = mapped_column(String(64), unique=True) value: Mapped[str] = mapped_column(Text, nullable=False)