6 KiB
Database
1. Status
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:
PRAGMA user_version;
Current schema:
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:
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:
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:
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:
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:
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:
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:
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:
ex_<uuid-v4>
se_<uuid-v4>
bo_<uuid-v4>
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:
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:
CC=clang meson setup build
meson compile -C build
meson test -C build --print-errorlogs
Sanitizer build:
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:
python tools/validate_json.py
python tools/validate_import_contract.py
git diff --check