trainlog/tui/include/trainlog/database.h

660 lines
32 KiB
C

/*
* Trainlog database interface.
*
* Declares the module boundary and ownership contract; implementation and persistence remain in
* their owning modules.
*/
#ifndef TRAINLOG_DATABASE_H
#define TRAINLOG_DATABASE_H
/**
* @file database.h
* @brief SQLite persistence API for the Trainlog TUI core.
*/
#include <stddef.h>
#include "trainlog/model.h"
#include "trainlog/status.h"
#define TRAINLOG_DATABASE_SCHEMA_VERSION 37
typedef struct TrainlogDatabase TrainlogDatabase;
#define TRAINLOG_FEEDBACK_TEXT_MAX 8192U
#define TRAINLOG_FEEDBACK_VIEW_MAX 4096U
typedef struct TrainlogFeedbackView {
char stable_id[TRAINLOG_ID_MAX + 1U];
char entry_id[TRAINLOG_ID_MAX + 1U]; /* empty for session follow-up */
char observed_at[TRAINLOG_TIMESTAMP_MAX + 1U];
char raw_text[TRAINLOG_FEEDBACK_TEXT_MAX + 1U];
} TrainlogFeedbackView;
/* CONTRACT: read-only chronological consultation; Android remains the sole
* V1 creator. Ties use the stable ID byte order. No SQLite resources escape. */
TrainlogStatus trainlog_database_list_training_feedback(TrainlogDatabase *database,
const char *session_id,
TrainlogFeedbackView *exercise_feedback,
size_t exercise_capacity,
size_t *exercise_count,
TrainlogFeedbackView *followups,
size_t followup_capacity,
size_t *followup_count);
/* Nestable read savepoints let composition services observe one database state
* without exposing SQLite or issuing writes to application data. */
TrainlogStatus trainlog_database_read_snapshot_begin(TrainlogDatabase *database);
TrainlogStatus trainlog_database_read_snapshot_end(TrainlogDatabase *database,
bool commit_snapshot);
/* WHY: session occurrences retain an equipment ID, while custom definitions
* need durable presentation metadata. A reference alone is never a definition. */
typedef struct TrainlogCustomEquipment {
char equipment_id[TRAINLOG_ID_MAX + 1U];
char display_name[TRAINLOG_NAME_MAX + 1U];
char label_name[TRAINLOG_NAME_MAX + 1U];
char equipment_type[TRAINLOG_NAME_MAX + 1U];
char load_semantics[32];
} TrainlogCustomEquipment;
#define TRAINLOG_CUSTOM_EQUIPMENT_PAGE_MAX 128U
typedef enum TrainlogEquipmentOrigin {
TRAINLOG_EQUIPMENT_SUPPLIED = 0,
TRAINLOG_EQUIPMENT_CUSTOM,
TRAINLOG_EQUIPMENT_UNKNOWN
} TrainlogEquipmentOrigin;
typedef struct TrainlogResolvedEquipment {
char equipment_id[TRAINLOG_ID_MAX + 1U];
char display_name[TRAINLOG_NAME_MAX + 1U];
char label_name[TRAINLOG_NAME_MAX + 1U];
char equipment_type[TRAINLOG_NAME_MAX + 1U];
char load_semantics[32];
TrainlogEquipmentOrigin origin;
} TrainlogResolvedEquipment;
TrainlogStatus trainlog_database_create_custom_equipment(TrainlogDatabase *database,
const TrainlogCustomEquipment *equipment);
TrainlogStatus trainlog_database_list_custom_equipment(TrainlogDatabase *database,
TrainlogCustomEquipment *output,
size_t capacity,
size_t *output_count);
/* CONTRACT: reads at most capacity + 1 ordered definitions, so callers can
* filter pages before imposing a presentation cap. database and output are
* borrowed for the call; the caller owns copied strings. This read-only API
* retains no resources and writes no persistence. capacity is 1..128 and all
* pointer arguments are required. output_count is at most capacity; output_more
* is true exactly when one additional ordered row exists. CONTRACT: output,
* output_count, and output_more are valid only when this returns
* TRAINLOG_STATUS_OK; on every other status callers must disregard all output,
* including any storage copied before the failure. offset is valid only while
* data is unchanged; refresh after writes. It is neither a durable cursor nor
* an idempotency mechanism. */
TrainlogStatus trainlog_database_list_custom_equipment_page(TrainlogDatabase *database,
size_t offset,
TrainlogCustomEquipment *output,
size_t capacity,
size_t *output_count,
bool *output_more);
/* CONTRACT: an occurrence ID always resolves to a visible value. Unknown IDs
* are returned verbatim with TRAINLOG_EQUIPMENT_UNKNOWN, never hidden. */
TrainlogStatus trainlog_database_resolve_equipment(TrainlogDatabase *database,
const char *equipment_id,
TrainlogResolvedEquipment *output);
TrainlogStatus trainlog_database_list_exercise_equipment(TrainlogDatabase *database,
const char *exercise_id,
TrainlogResolvedEquipment *output,
size_t capacity,
size_t *output_count);
TrainlogStatus trainlog_database_open(const char *path, TrainlogDatabase **output_database);
/**
* @brief Open a database and report the backend failure that prevented it.
*
* CONTRACT: @p output_diagnostic is optional. When supplied with a non-zero
* capacity, it receives a NUL-terminated explanation of the failed SQLite
* operation; callers still use the returned TrainlogStatus for control flow.
* This preserves the stable application status surface without hiding the
* path-specific reason needed to repair a user's durable database safely.
*/
TrainlogStatus trainlog_database_open_with_diagnostic(const char *path,
TrainlogDatabase **output_database,
char *output_diagnostic,
size_t output_diagnostic_capacity);
void trainlog_database_close(TrainlogDatabase *database);
TrainlogStatus trainlog_database_schema_version(TrainlogDatabase *database, int *output_version);
TrainlogStatus trainlog_database_foreign_keys_enabled(TrainlogDatabase *database,
int *output_enabled);
TrainlogStatus trainlog_database_begin(TrainlogDatabase *database);
TrainlogStatus trainlog_database_commit(TrainlogDatabase *database);
TrainlogStatus trainlog_database_rollback(TrainlogDatabase *database);
/**
* @brief Insert one already-normalized canonical exercise row.
*
* Unicode normalization belongs to catalog.c. This lower-level API owns the
* final SQLite uniqueness barrier.
*/
TrainlogStatus trainlog_database_insert_exercise(TrainlogDatabase *database,
const char *exercise_id,
const char *name,
const char *normalized_name,
TrainlogTrackingMode tracking_mode);
TrainlogStatus trainlog_database_insert_exercise_profiled(TrainlogDatabase *database,
const char *exercise_id,
const char *name,
const char *normalized_name,
TrainlogTrackingMode tracking_mode,
TrainlogRecordingMode recording_mode,
TrainlogExerciseDataFields data_fields);
TrainlogStatus trainlog_database_exercise_count(TrainlogDatabase *database, size_t *output_count);
TrainlogStatus trainlog_database_list_exercises(TrainlogDatabase *database,
TrainlogExercise *output,
size_t capacity,
size_t *output_count);
/**
* @brief Resolve a current or merged exercise identity to its canonical ID.
*
* CONTRACT: current catalogue IDs resolve to themselves; durable legacy IDs
* resolve through exercise_aliases. Unknown IDs return NOT_FOUND. The output
* buffer is caller-owned and must hold TRAINLOG_ID_MAX + 1 bytes.
*/
TrainlogStatus trainlog_database_resolve_exercise_id(TrainlogDatabase *database,
const char *exercise_id,
char *output_canonical_id,
size_t output_capacity);
/**
* @brief Atomically merge one current catalogue exercise into another.
*
* CONTRACT: source and canonical must be distinct current catalogue IDs.
* Tracking/recording/data-field or conflicting non-empty primary-zone
* profiles reject with CONFLICT and leave the database untouched. Compatible
* direct zones are unioned; every occurrence is repointed without rewriting
* its stable entry ID or any owned actual/planning data. Existing aliases to
* source collapse directly to canonical and source becomes a durable alias.
*/
TrainlogStatus trainlog_database_merge_exercises(TrainlogDatabase *database,
const char *source_exercise_id,
const char *canonical_exercise_id);
typedef struct TrainlogExerciseMergePreview {
size_t occurrences;
size_t performed_sets;
size_t continuous_activities;
size_t max_results;
size_t associated_equipment;
size_t body_zones;
} TrainlogExerciseMergePreview;
/* CONTRACT: counts describe source-owned records which the atomic merge will
* repoint or union. Unknown IDs return NOT_FOUND and output is never partial. */
TrainlogStatus trainlog_database_preview_exercise_merge(TrainlogDatabase *database,
const char *source_exercise_id,
TrainlogExerciseMergePreview *output);
/**
* @brief Atomically replace one exercise's direct body-zone relations.
*
* @param primary_zone_id NULL or empty preserves the explicitly allowed
* unclassified state, which requires secondary_count == 0. Group zones
* cannot be assigned because their descendant relationship is derived
* from the canonical manifest.
* @param secondary_zone_ids Caller-owned array borrowed for this call only.
* @param secondary_count Bounded by the number of canonical manifest zones.
* @return INVALID_ARGUMENT for unknown/group/duplicate zones, NOT_FOUND for
* an unknown exercise, or the explicit persistence status.
*/
TrainlogStatus trainlog_database_replace_exercise_body_zones(TrainlogDatabase *database,
const char *exercise_id,
const char *primary_zone_id,
const char *const *secondary_zone_ids,
size_t secondary_count);
/* NOT_FOUND distinguishes an unknown exercise identity from the valid empty
* relation set. Output is caller-owned; output_count reports required capacity
* and no relation is truncated as a successful result. */
TrainlogStatus trainlog_database_list_exercise_body_zones(TrainlogDatabase *database,
const char *exercise_id,
TrainlogExerciseBodyZone *output,
size_t capacity,
size_t *output_count);
/* CONTRACT: the caller owns output storage; output_count reports the required
* count and INVALID_ARGUMENT is returned when capacity is insufficient.
* Filtering combines a normalized name prefix with either one
* direct zone, its manifest descendants, or the explicit unclassified state.
* `primary_only` excludes secondary participation without changing storage.
* zone_id and unclassified_only are mutually exclusive.
* The resulting exercise IDs compose with list_exercise_body_zones() and
* list_exercise_performance(); the latter exposes newest history and explicit
* MAX without duplicating either datum for a future session generator. */
TrainlogStatus trainlog_database_list_exercises_filtered(TrainlogDatabase *database,
const char *normalized_prefix,
const char *zone_id,
bool include_descendants,
bool primary_only,
bool unclassified_only,
TrainlogExercise *output,
size_t capacity,
size_t *output_count);
/* CONTRACT: name/profile/direct zones are one transaction and all string/list
* inputs are borrowed only for the duration of the call. Profile changes affect
* future occurrences only because schema v16 snapshots the complete profile on
* every occurrence. Existing sessions and drafts are never rewritten. The
* function owns a transaction when called in autocommit mode and participates
* in an already-open caller transaction otherwise. Zone rules match
* replace_exercise_body_zones(). Unknown exercise IDs return NOT_FOUND; invalid
* metadata and name collisions remain explicit errors. */
TrainlogStatus trainlog_database_update_exercise_profiled(TrainlogDatabase *database,
const char *exercise_id,
const char *name,
const char *normalized_name,
TrainlogTrackingMode tracking_mode,
TrainlogRecordingMode recording_mode,
TrainlogExerciseDataFields data_fields,
const char *primary_zone_id,
const char *const *secondary_zone_ids,
size_t secondary_count);
TrainlogStatus trainlog_database_insert_session(TrainlogDatabase *database,
const TrainlogSessionInput *session);
TrainlogStatus trainlog_database_session_count(TrainlogDatabase *database, size_t *output_count);
/* Observable history only: a returned session owns at least one persisted
* performed set, continuous activity, or explicit MAX result. ended_at is
* presentation metadata and is not an actual-work gate. */
TrainlogStatus trainlog_database_list_sessions(TrainlogDatabase *database,
TrainlogSessionSummary *output,
size_t capacity,
size_t *output_count);
TrainlogStatus
trainlog_database_insert_body_observation(TrainlogDatabase *database,
const TrainlogBodyObservationInput *observation);
TrainlogStatus trainlog_database_list_weight_points(TrainlogDatabase *database,
TrainlogWeightPoint *output,
size_t capacity,
size_t *output_count);
/* TRAINLOG_SESSION_DETAILS_API */
#define TRAINLOG_SET_SUMMARY_MAX 1024U
typedef struct TrainlogPersistedExerciseDetail {
/* Stable occurrence identity; exercise_id is catalogue identity only. */
char entry_id[TRAINLOG_ID_MAX + 1U];
char name[TRAINLOG_NAME_MAX + 1U];
char equipment_id[TRAINLOG_ID_MAX + 1U];
TrainlogTrackingMode tracking_mode;
TrainlogRecordingMode recording_mode;
TrainlogExerciseDataFields data_fields;
TrainlogLoadMode load_mode;
int rest_seconds;
int target_sets;
int target_reps;
int target_duration_seconds;
int has_target_weight;
double target_weight_kg;
int has_max_weight;
double max_weight_kg;
int continuous_duration_seconds;
int has_continuous_speed;
double continuous_speed_kmh;
int has_continuous_distance;
double continuous_distance_km;
size_t actual_set_count;
/* CONTRACT: history consumes this occurrence-owned ordered snapshot;
* rendering must not reach into SQLite or impose a global dataset cap. */
TrainlogSetInput *actual_sets;
char actual_summary[TRAINLOG_SET_SUMMARY_MAX + 1U];
} TrainlogPersistedExerciseDetail;
/**
* @brief Load one session header plus ordered exercise details.
*
* Callers must release a prior successful result before reusing its array.
* On success, each copied detail owns actual_sets until released with
* trainlog_database_free_session_details(). On failure, the function releases
* every partial allocation and, when output_exercise_count is non-NULL, sets
* it to zero. A nonzero capacity requires a non-NULL output_exercises array.
*/
TrainlogStatus
trainlog_database_get_session_details(TrainlogDatabase *database,
const char *session_id,
TrainlogSessionSummary *output_session,
TrainlogPersistedExerciseDetail *output_exercises,
size_t exercise_capacity,
size_t *output_exercise_count);
/* Safe for zero-initialized details and details returned by the loader. */
void trainlog_database_free_session_details(TrainlogPersistedExerciseDetail *exercises,
size_t exercise_count);
/* TRAINLOG_BODY_METRIC_HISTORY_API */
typedef enum TrainlogBodyMetric {
TRAINLOG_BODY_METRIC_WEIGHT = 0,
TRAINLOG_BODY_METRIC_NECK,
TRAINLOG_BODY_METRIC_SHOULDERS,
TRAINLOG_BODY_METRIC_CHEST,
TRAINLOG_BODY_METRIC_WAIST,
TRAINLOG_BODY_METRIC_HIPS,
TRAINLOG_BODY_METRIC_LEFT_ARM,
TRAINLOG_BODY_METRIC_RIGHT_ARM,
TRAINLOG_BODY_METRIC_LEFT_FOREARM,
TRAINLOG_BODY_METRIC_RIGHT_FOREARM,
TRAINLOG_BODY_METRIC_LEFT_THIGH,
TRAINLOG_BODY_METRIC_RIGHT_THIGH,
TRAINLOG_BODY_METRIC_LEFT_CALF,
TRAINLOG_BODY_METRIC_RIGHT_CALF,
TRAINLOG_BODY_METRIC_COUNT
} TrainlogBodyMetric;
typedef struct TrainlogBodyMetricPoint {
char observed_at[TRAINLOG_TIMESTAMP_MAX + 1U];
double value;
} TrainlogBodyMetricPoint;
typedef struct TrainlogBodyPairPoint {
bool found;
char observed_at[TRAINLOG_TIMESTAMP_MAX + 1U];
double left_value;
double right_value;
} TrainlogBodyPairPoint;
TrainlogStatus trainlog_database_list_body_metric_points(TrainlogDatabase *database,
TrainlogBodyMetric metric,
TrainlogBodyMetricPoint *output,
size_t capacity,
size_t *output_count);
TrainlogStatus trainlog_database_latest_body_pair(TrainlogDatabase *database,
TrainlogBodyMetric left_metric,
TrainlogBodyMetric right_metric,
TrainlogBodyPairPoint *output);
/* TRAINLOG_EXERCISE_PERFORMANCE_API */
typedef struct TrainlogExercisePerformancePoint {
char session_id[TRAINLOG_ID_MAX + 1U];
char started_at[TRAINLOG_TIMESTAMP_MAX + 1U];
TrainlogSessionType session_type;
TrainlogTrackingMode tracking_mode;
TrainlogLoadMode load_mode;
size_t actual_set_count;
int has_performance;
/* Explicit schema-v9 max: weight is the result; metric_value is internal. */
int has_explicit_max;
int metric_value;
int has_weight;
double weight_kg;
char equipment_id[TRAINLOG_ID_MAX + 1U];
} TrainlogExercisePerformancePoint;
/**
* @brief Read newest-first per-session representative performance.
*
* Representative-set semantics:
*
* - no load: greatest successful reps/duration;
* - external load: greatest load, then greatest reps/duration;
* - assistance: lowest assistance, then greatest reps/duration.
*
* A zero-repetition failed attempt is never promoted to representative
* performance. This API does not create or infer a measured maximum.
*/
TrainlogStatus trainlog_database_list_exercise_performance(TrainlogDatabase *database,
const char *exercise_id,
TrainlogExercisePerformancePoint *output,
size_t capacity,
size_t *output_count);
/* TRAINING_KNOWLEDGE_RUNTIME_READ_V1 */
#define TRAINLOG_OCCURRENCE_PAGE_MAX 32U
#define TRAINLOG_OCCURRENCE_SET_PAGE_MAX 64U
typedef struct TrainlogExerciseOccurrenceCursor {
char started_at[TRAINLOG_TIMESTAMP_MAX + 1U];
char session_id[TRAINLOG_ID_MAX + 1U];
char entry_id[TRAINLOG_ID_MAX + 1U];
} TrainlogExerciseOccurrenceCursor;
typedef struct TrainlogExerciseOccurrence {
char session_id[TRAINLOG_ID_MAX + 1U];
char entry_id[TRAINLOG_ID_MAX + 1U];
char exercise_id[TRAINLOG_ID_MAX + 1U];
char started_at[TRAINLOG_TIMESTAMP_MAX + 1U];
char equipment_id[TRAINLOG_ID_MAX + 1U];
TrainlogSessionType session_type;
TrainlogTrackingMode tracking_mode;
TrainlogRecordingMode recording_mode;
TrainlogExerciseDataFields data_fields;
TrainlogLoadMode load_mode;
size_t set_count;
int continuous_duration_seconds;
bool continuous_has_speed;
double continuous_speed_kmh;
bool continuous_has_distance;
double continuous_distance_km;
} TrainlogExerciseOccurrence;
typedef struct TrainlogOccurrenceSet {
size_t position;
bool has_reps;
int reps;
bool has_duration;
int duration_seconds;
bool has_weight;
double weight_kg;
} TrainlogOccurrenceSet;
typedef struct TrainlogLatestExplicitMax {
bool found;
char session_id[TRAINLOG_ID_MAX + 1U];
char entry_id[TRAINLOG_ID_MAX + 1U];
char started_at[TRAINLOG_TIMESTAMP_MAX + 1U];
char equipment_id[TRAINLOG_ID_MAX + 1U];
TrainlogLoadMode load_mode;
double max_weight_kg;
} TrainlogLatestExplicitMax;
/* Exact-ID profile read; unknown IDs return NOT_FOUND. */
TrainlogStatus trainlog_database_get_exercise_profile(TrainlogDatabase *database,
const char *exercise_id,
TrainlogExercise *output);
/**
* Current-data keyset page ordered by the exact started_at instant, then
* session_id and entry_id bytewise DESC. Accepted timestamps use the frozen
* extended RFC3339 grammar, including T/t, Z/z, numeric offsets through
* 23:59, arbitrary fractional precision, and the Android writer's omitted
* seconds form. Equivalent trailing-zero fractions represent one instant.
* A non-NULL cursor is exclusive. Pages are not a cross-call snapshot under
* concurrent edits. Cursor arrays are borrowed for this call, must be NUL
* terminated within their declared capacities, and started_at must be a real
* accepted instant with an explicit numeric offset or Z/z. Malformed cursors
* are INVALID_ARGUMENT before any history query. Malformed matching persisted
* timestamps, or a selected timestamp too long for the fixed public output,
* are DATABASE_ERROR rather than omitted or truncated. limit must be
* 1..TRAINLOG_OCCURRENCE_PAGE_MAX.
*/
TrainlogStatus
trainlog_database_list_exercise_occurrences_page(TrainlogDatabase *database,
const char *exercise_id,
const TrainlogExerciseOccurrenceCursor *after,
size_t limit,
TrainlogExerciseOccurrence *output,
size_t *output_count,
bool *output_has_more,
TrainlogExerciseOccurrenceCursor *output_next);
/* Original set positions are preserved. after_position is exclusive; use -1
* for the first page. Reps are non-negative, so zero preserves a failed
* attempt. Nullable values remain distinct from zero. Corrupt storage types or
* values outside the public int/size_t ranges return DATABASE_ERROR. No rows
* exist for continuous occurrences. Output remains caller-owned. */
TrainlogStatus trainlog_database_list_occurrence_sets_page(TrainlogDatabase *database,
const char *entry_id,
int after_position,
size_t limit,
TrainlogOccurrenceSet *output,
size_t *output_count,
bool *output_has_more,
int *output_next_position);
/* Reads max_results joined only through max_test sessions; ordinary large sets
* never qualify. It uses the same exact instant and bytewise-ID ordering as
* occurrence pagination. Output keeps the exact source timestamp and
* occurrence load/equipment context; corrupt or over-capacity selected storage
* returns DATABASE_ERROR. */
TrainlogStatus trainlog_database_latest_explicit_max_context(TrainlogDatabase *database,
const char *exercise_id,
TrainlogLatestExplicitMax *output);
/* Same chronological contract, additionally restricted to one exact nonempty
* equipment ID. This is the compatibility reader for user-directed %MAX. */
TrainlogStatus
trainlog_database_latest_explicit_max_equipment_context(TrainlogDatabase *database,
const char *exercise_id,
const char *equipment_id,
TrainlogLatestExplicitMax *output);
/* TRAINLOG_SESSION_EDIT_API */
typedef struct TrainlogEditableExerciseRecord {
char entry_id[TRAINLOG_ID_MAX + 1U];
char exercise_id[TRAINLOG_ID_MAX + 1U];
char equipment_id[TRAINLOG_ID_MAX + 1U];
char name[TRAINLOG_NAME_MAX + 1U];
TrainlogTrackingMode tracking_mode;
TrainlogRecordingMode recording_mode;
TrainlogExerciseDataFields data_fields;
TrainlogLoadMode load_mode;
int rest_seconds;
int target_sets;
int target_reps;
int target_duration_seconds;
int has_target_weight;
double target_weight_kg;
int has_max_weight;
double max_weight_kg;
int continuous_duration_seconds;
int has_continuous_speed;
double continuous_speed_kmh;
int has_continuous_distance;
double continuous_distance_km;
char notes[TRAINLOG_NOTE_MAX + 1U];
size_t set_offset;
size_t set_count;
} TrainlogEditableExerciseRecord;
/**
* @brief Load a complete persisted session into caller-owned editable buffers.
*
* The function never truncates a session silently. If either caller capacity is
* too small it returns TRAINLOG_STATUS_INVALID_ARGUMENT and the outputs must not
* be used.
*/
TrainlogStatus
trainlog_database_load_session_editable(TrainlogDatabase *database,
const char *session_id,
TrainlogSessionSummary *output_session,
TrainlogEditableExerciseRecord *output_exercises,
size_t exercise_capacity,
size_t *output_exercise_count,
TrainlogSetInput *output_sets,
size_t set_capacity,
size_t *output_set_count);
/**
* @brief Replace only the exercise/set contents of an existing session.
*
* The session row itself is preserved, so session_id, timestamps, session type,
* session notes, session follow-ups and body_observations.session_row_id remain
* attached to the same row. Exercise feedback is remapped only by retained
* entry_id within that session; removed entries cascade and new entries never
* inherit feedback. The replacement is atomic: any error rolls the whole
* operation back.
*/
TrainlogStatus
trainlog_database_replace_session_exercises(TrainlogDatabase *database,
const char *session_id,
const TrainlogSessionExerciseInput *exercises,
size_t exercise_count);
/* TRAINLOG_BODY_OBSERVATION_RECORD_API */
typedef struct TrainlogBodyObservationRecord {
char observation_id[TRAINLOG_ID_MAX + 1U];
char observed_at[TRAINLOG_TIMESTAMP_MAX + 1U];
char session_id[TRAINLOG_ID_MAX + 1U];
bool has_body_weight;
double body_weight_kg;
bool has_neck;
double neck_cm;
bool has_shoulders;
double shoulders_cm;
bool has_chest;
double chest_cm;
bool has_waist;
double waist_cm;
bool has_hips;
double hips_cm;
bool has_left_arm;
double left_arm_cm;
bool has_right_arm;
double right_arm_cm;
bool has_left_forearm;
double left_forearm_cm;
bool has_right_forearm;
double right_forearm_cm;
bool has_left_thigh;
double left_thigh_cm;
bool has_right_thigh;
double right_thigh_cm;
bool has_left_calf;
double left_calf_cm;
bool has_right_calf;
double right_calf_cm;
char notes[TRAINLOG_NOTE_MAX + 1U];
} TrainlogBodyObservationRecord;
TrainlogStatus trainlog_database_list_body_observations(TrainlogDatabase *database,
TrainlogBodyObservationRecord *output,
size_t capacity,
size_t *output_count);
TrainlogStatus trainlog_database_get_body_observation(TrainlogDatabase *database,
const char *observation_id,
TrainlogBodyObservationRecord *output);
/**
* @brief Correct metric values of one existing observation.
*
* observation_id identifies the stable row. observed_at and session linkage
* remain unchanged. At least one body metric must remain present.
*/
TrainlogStatus
trainlog_database_update_body_observation(TrainlogDatabase *database,
const TrainlogBodyObservationInput *observation);
#endif