trainlog/docs/database.md

6.7 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

Exercise recording metadata — schema v3 direction

The next SQLite migration adds:

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.