# Database ## 1. Status ```text GATE_2=IN_PROGRESS DATABASE_SCHEMA_V2=IMPLEMENTED SESSION_TYPE_PERSISTENCE=IMPLEMENTED SESSION_EDIT_PERSISTENCE=IMPLEMENTED BODY_OBSERVATION_EDIT=IMPLEMENTED TRAINLOG_FORMAT_V1=FROZEN ``` Gate 2 review #1 establishes the persistence foundation. The Trainlog exchange format v1 is already frozen and is not modified by this gate. ## 2. Purpose SQLite is the canonical long-term store used by the TUI. The SQLite database is an internal persistence format and is versioned independently from the Trainlog JSON exchange format. ## 3. Schema versioning Trainlog database schema version uses SQLite: ```sql PRAGMA user_version; ``` Current schema: ```text DATABASE_SCHEMA_V2=2 ``` A new database starts with `user_version = 0` and is initialized atomically to the current schema. The implemented historical path is: ```text 0 -> 2 fresh initialization 1 -> 2 transactional migration ``` Schema v2 adds local session classification while leaving the frozen Trainlog JSON v1 exchange contract unchanged. A database newer than the running binary understands is rejected. Every future schema change requires an explicit migration and dedicated coverage; metadata-only version rewriting is not an accepted migration. ## 4. Connection rules Every Trainlog SQLite connection must enable: ```sql PRAGMA foreign_keys = ON; ``` The core also configures a bounded SQLite busy timeout. Foreign-key activation is verified by tests. ## 5. Tables ### 5.1 `exercises` Canonical exercise catalog. Fields: ```text id internal INTEGER primary key exercise_id stable Trainlog identity, UNIQUE name display name normalized_name v1 comparison form, UNIQUE tracking_mode reps | duration ``` The database does not compute Unicode normalization in review #1. The application/catalog layer will compute the frozen v1 normalized name in Gate 2 review #2. SQLite owns the final uniqueness barrier. ### 5.2 `sessions` Canonical workout session header. Fields: ```text id session_id UNIQUE started_at ended_at nullable session_type training | max_test notes nullable ``` `session_type` is a local SQLite concern in schema v2. Existing schema-v1 rows migrate to `training`; no historical workout is retroactively inferred to be a max test. Body data is stored separately so standalone body observations can use the same representation. ### 5.3 `session_exercises` One ordered exercise within a session. Fields include: ```text session_row_id exercise_row_id position load_mode rest_seconds target_sets target_reps target_duration_seconds target_weight_kg notes ``` Constraints enforce: - one exercise identity at most once per session; - one row at each session position; - exactly one target metric: repetitions or duration; - target load presence consistent with `load_mode`. ### 5.4 `performed_sets` Ordered actual sets. Fields: ```text session_exercise_row_id position reps duration_seconds weight_kg ``` Exactly one of repetitions or duration is present. Cross-table rules such as actual-set load consistency with the owning session exercise remain application/import invariants and will be tested at the import layer. ### 5.5 `body_observations` Body history is a first-class database concept and may exist with or without a workout session. Fields include: ```text observation_id observed_at session_row_id optional and UNIQUE body_weight_kg neck_cm shoulders_cm chest_cm waist_cm hips_cm left_arm_cm right_arm_cm left_forearm_cm right_forearm_cm left_thigh_cm right_thigh_cm left_calf_cm right_calf_cm notes ``` At least one body metric must be present. Imported session-associated body data will create one linked observation. Standalone TUI measurements use the same table without `session_row_id`. ## 6. UUID generation Official Trainlog creators generate UUID version 4 identifiers. The C core provides generated IDs for: ```text ex_ se_ bo_ ``` This is creation policy. The frozen exchange parser remains able to accept other schema-valid opaque v1 identifiers. ## 7. Transactions Multi-row operations are atomic. Gate 2 provides explicit: ```text BEGIN IMMEDIATE COMMIT ROLLBACK ``` primitives. The JSON import service must perform catalog reconciliation and all session inserts inside one transaction. Persisted session correction also uses an explicit transaction. Editing a session replaces only its `session_exercises` / `performed_sets` children and preserves the parent session row, stable `session_id`, timestamps, `session_type`, session notes, and any linked body observation. Body-observation correction preserves observation identity, timestamp, and optional session link. A hard conflict or validation failure leaves the database unchanged. ## 8. Units Canonical persistent units remain: - weight/load: kilograms; - body circumference: centimeters; - duration/rest: seconds. ## 9. Current Gate 2 persistence boundary Implemented persistence includes: - SQLite schema v2; - transactional schema migration v1 -> v2; - Unicode-aware canonical exercise catalog support; - complete session insertion; - session detail loading; - exact bounded editable-session loading; - transactional replacement of session exercise/set children; - body-observation creation, listing, exact lookup, and update; - stable local identifiers for exercises, sessions, and body observations. The database remains independent from ncurses rendering. The frozen Trainlog JSON v1 format remains a separate compatibility boundary and is not version-coupled to SQLite schema v2. ## 10. Validation Normal build: ```bash CC=clang meson setup build meson compile -C build meson test -C build --print-errorlogs ``` Sanitizer build: ```bash CC=clang meson setup build-asan \ -Db_sanitize=address,undefined \ -Db_lundef=false meson compile -C build-asan meson test -C build-asan --print-errorlogs ``` Repository-level format validators remain mandatory: ```bash python tools/validate_json.py python tools/validate_import_contract.py git diff --check ``` ## Exercise recording metadata — schema v3 direction The next SQLite migration adds: ```text recording_mode = sets | continuous data_fields = bounded bit mask ``` Existing `tracking_mode = reps | duration` remains stable. Every `session_exercises` row snapshots this metadata so later catalog changes do not reinterpret historical sessions. Migration v2 -> v3 defaults all existing rows to `sets` with `data_fields = 0`. No name-based migration is allowed. Continuous actual activity data is stored separately from `performed_sets`; Trainlog will not manufacture a fake one-set workout. ## Schema v4 — continuous exercise persistence Current database schema: ```text TRAINLOG_DATABASE_SCHEMA_VERSION = 4 PRAGMA user_version = 4 ``` Relevant profile metadata is stored in both: ```text exercises session_exercises ``` `session_exercises` snapshots: ```text recording_mode data_fields ``` Continuous actual activity data is stored one-to-one in: ```text continuous_activity ``` Columns: ```text session_exercise_row_id duration_seconds speed_kmh nullable distance_km nullable ``` For a valid continuous activity: ```text target_sets NULL target_reps NULL target_duration_seconds NULL load_mode none rest_seconds 0 target_weight_kg NULL performed_sets none ``` Schema migration never infers profile information from exercise names.