5108 lines
139 KiB
C
5108 lines
139 KiB
C
/**
|
|
* @file database.c
|
|
* @brief SQLite persistence implementation for Trainlog.
|
|
*/
|
|
|
|
#include "trainlog/database.h"
|
|
#include "trainlog/duration.h"
|
|
#include "trainlog/equipment_catalog.h"
|
|
#include "trainlog/id.h"
|
|
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include <sqlite3.h>
|
|
|
|
struct TrainlogDatabase {
|
|
sqlite3 *connection;
|
|
};
|
|
|
|
static bool custom_equipment_exists(TrainlogDatabase *database, const char *equipment_id)
|
|
{
|
|
sqlite3_stmt *statement = NULL;
|
|
int rc;
|
|
bool found = false;
|
|
if (database == NULL || equipment_id == NULL) return false;
|
|
rc = sqlite3_prepare_v2(database->connection,
|
|
"SELECT 1 FROM custom_equipment WHERE equipment_id=?1;", -1, &statement, NULL);
|
|
if (rc != SQLITE_OK) return false;
|
|
(void)sqlite3_bind_text(statement, 1, equipment_id, -1, SQLITE_TRANSIENT);
|
|
found = sqlite3_step(statement) == SQLITE_ROW;
|
|
(void)sqlite3_finalize(statement);
|
|
return found;
|
|
}
|
|
|
|
static void set_open_diagnostic(
|
|
char *output,
|
|
size_t capacity,
|
|
const char *operation,
|
|
sqlite3 *connection,
|
|
int sqlite_status
|
|
)
|
|
{
|
|
const char *message;
|
|
int extended_status = sqlite_status;
|
|
|
|
if (output == NULL || capacity == 0U) {
|
|
return;
|
|
}
|
|
|
|
if (connection != NULL) {
|
|
message = sqlite3_errmsg(connection);
|
|
sqlite_status = sqlite3_errcode(connection);
|
|
extended_status = sqlite3_extended_errcode(connection);
|
|
} else {
|
|
message = sqlite3_errstr(sqlite_status);
|
|
}
|
|
/* WHY: preserve the actionable SQLite result while the owning connection
|
|
* is still open; rollback/close may replace its diagnostic state. */
|
|
(void)snprintf(
|
|
output,
|
|
capacity,
|
|
"%s: SQLite rc=%d extended_rc=%d: %s",
|
|
operation,
|
|
sqlite_status,
|
|
extended_status,
|
|
message
|
|
);
|
|
}
|
|
|
|
static void set_application_diagnostic(
|
|
char *output,
|
|
size_t capacity,
|
|
const char *message
|
|
)
|
|
{
|
|
if (output == NULL || capacity == 0U) {
|
|
return;
|
|
}
|
|
(void)snprintf(output, capacity, "%s", message);
|
|
}
|
|
|
|
static const char *const SCHEMA_V7_SQL_A =
|
|
"BEGIN IMMEDIATE;"
|
|
|
|
"CREATE TABLE IF NOT EXISTS exercises ("
|
|
" id INTEGER PRIMARY KEY,"
|
|
" exercise_id TEXT NOT NULL UNIQUE,"
|
|
" name TEXT NOT NULL,"
|
|
" normalized_name TEXT NOT NULL UNIQUE,"
|
|
" tracking_mode TEXT NOT NULL"
|
|
" CHECK (tracking_mode IN ('reps', 'duration')),"
|
|
" recording_mode TEXT NOT NULL DEFAULT 'sets'"
|
|
" CHECK (recording_mode IN ('sets', 'continuous')),"
|
|
" data_fields INTEGER NOT NULL DEFAULT 0"
|
|
" CHECK (data_fields >= 0 AND (data_fields & ~3) = 0),"
|
|
" CHECK (recording_mode != 'continuous' OR"
|
|
" tracking_mode = 'duration')"
|
|
");"
|
|
|
|
"CREATE TABLE IF NOT EXISTS sessions ("
|
|
" id INTEGER PRIMARY KEY,"
|
|
" session_id TEXT NOT NULL UNIQUE,"
|
|
" started_at TEXT NOT NULL,"
|
|
" ended_at TEXT,"
|
|
" session_type TEXT NOT NULL DEFAULT 'training'"
|
|
" CHECK (session_type IN ('training', 'max_test')),"
|
|
" notes TEXT"
|
|
");"
|
|
|
|
"CREATE TABLE IF NOT EXISTS session_exercises ("
|
|
" id INTEGER PRIMARY KEY,"
|
|
" entry_id TEXT NOT NULL UNIQUE,"
|
|
" session_row_id INTEGER NOT NULL"
|
|
" REFERENCES sessions(id) ON DELETE CASCADE,"
|
|
" exercise_row_id INTEGER NOT NULL"
|
|
" REFERENCES exercises(id) ON DELETE RESTRICT,"
|
|
" recording_mode TEXT NOT NULL DEFAULT 'sets'"
|
|
" CHECK (recording_mode IN ('sets', 'continuous')),"
|
|
" data_fields INTEGER NOT NULL DEFAULT 0"
|
|
" CHECK (data_fields >= 0 AND (data_fields & ~3) = 0),"
|
|
" position INTEGER NOT NULL CHECK (position >= 0),"
|
|
" load_mode TEXT NOT NULL"
|
|
" CHECK (load_mode IN ('none', 'external', 'assistance')),"
|
|
" rest_seconds INTEGER NOT NULL CHECK (rest_seconds >= 0),"
|
|
" target_sets INTEGER CHECK (target_sets > 0),"
|
|
" target_reps INTEGER CHECK (target_reps >= 1),"
|
|
" target_duration_seconds INTEGER"
|
|
" CHECK (target_duration_seconds > 0),"
|
|
" target_weight_kg REAL CHECK (target_weight_kg > 0.0),"
|
|
" equipment_id TEXT,"
|
|
" notes TEXT,"
|
|
" UNIQUE (session_row_id, position),"
|
|
" CHECK ("
|
|
" (recording_mode = 'sets' AND"
|
|
" ("
|
|
" (target_sets IS NULL AND"
|
|
" target_reps IS NULL AND"
|
|
" target_duration_seconds IS NULL) OR"
|
|
" (target_sets IS NOT NULL AND"
|
|
" ((target_reps IS NOT NULL AND"
|
|
" target_duration_seconds IS NULL) OR"
|
|
" (target_reps IS NULL AND"
|
|
" target_duration_seconds IS NOT NULL)))"
|
|
" )) OR"
|
|
" (recording_mode = 'continuous' AND"
|
|
" target_sets IS NULL AND"
|
|
" target_reps IS NULL AND"
|
|
" target_duration_seconds IS NULL AND"
|
|
" load_mode = 'none' AND"
|
|
" rest_seconds = 0 AND"
|
|
" target_weight_kg IS NULL)"
|
|
" ),"
|
|
" CHECK ("
|
|
" (load_mode = 'none' AND target_weight_kg IS NULL) OR"
|
|
" (load_mode IN ('external', 'assistance') AND"
|
|
" target_weight_kg IS NOT NULL)"
|
|
" )"
|
|
");"
|
|
|
|
"CREATE TABLE IF NOT EXISTS performed_sets ("
|
|
" id INTEGER PRIMARY KEY,"
|
|
" session_exercise_row_id INTEGER NOT NULL"
|
|
" REFERENCES session_exercises(id) ON DELETE CASCADE,"
|
|
" position INTEGER NOT NULL CHECK (position >= 0),"
|
|
" reps INTEGER CHECK (reps >= 0),"
|
|
" duration_seconds INTEGER CHECK (duration_seconds > 0),"
|
|
" weight_kg REAL CHECK (weight_kg > 0.0),"
|
|
" UNIQUE (session_exercise_row_id, position),"
|
|
" CHECK ("
|
|
" (reps IS NOT NULL AND duration_seconds IS NULL) OR"
|
|
" (reps IS NULL AND duration_seconds IS NOT NULL)"
|
|
" )"
|
|
");";
|
|
|
|
static const char *const SCHEMA_V7_SQL_B =
|
|
"CREATE TABLE IF NOT EXISTS continuous_activity ("
|
|
" id INTEGER PRIMARY KEY,"
|
|
" session_exercise_row_id INTEGER NOT NULL UNIQUE"
|
|
" REFERENCES session_exercises(id) ON DELETE CASCADE,"
|
|
" duration_seconds INTEGER NOT NULL CHECK (duration_seconds > 0),"
|
|
" speed_kmh REAL CHECK (speed_kmh > 0.0),"
|
|
" distance_km REAL CHECK (distance_km > 0.0)"
|
|
");"
|
|
|
|
"CREATE TABLE IF NOT EXISTS body_observations ("
|
|
" id INTEGER PRIMARY KEY,"
|
|
" observation_id TEXT NOT NULL UNIQUE,"
|
|
" observed_at TEXT NOT NULL,"
|
|
" session_row_id INTEGER UNIQUE"
|
|
" REFERENCES sessions(id) ON DELETE CASCADE,"
|
|
" body_weight_kg REAL CHECK (body_weight_kg > 0.0),"
|
|
" neck_cm REAL CHECK (neck_cm > 0.0),"
|
|
" shoulders_cm REAL CHECK (shoulders_cm > 0.0),"
|
|
" chest_cm REAL CHECK (chest_cm > 0.0),"
|
|
" waist_cm REAL CHECK (waist_cm > 0.0),"
|
|
" hips_cm REAL CHECK (hips_cm > 0.0),"
|
|
" left_arm_cm REAL CHECK (left_arm_cm > 0.0),"
|
|
" right_arm_cm REAL CHECK (right_arm_cm > 0.0),"
|
|
" left_forearm_cm REAL CHECK (left_forearm_cm > 0.0),"
|
|
" right_forearm_cm REAL CHECK (right_forearm_cm > 0.0),"
|
|
" left_thigh_cm REAL CHECK (left_thigh_cm > 0.0),"
|
|
" right_thigh_cm REAL CHECK (right_thigh_cm > 0.0),"
|
|
" left_calf_cm REAL CHECK (left_calf_cm > 0.0),"
|
|
" right_calf_cm REAL CHECK (right_calf_cm > 0.0),"
|
|
" notes TEXT,"
|
|
" CHECK ("
|
|
" body_weight_kg IS NOT NULL OR"
|
|
" neck_cm IS NOT NULL OR shoulders_cm IS NOT NULL OR"
|
|
" chest_cm IS NOT NULL OR waist_cm IS NOT NULL OR"
|
|
" hips_cm IS NOT NULL OR left_arm_cm IS NOT NULL OR"
|
|
" right_arm_cm IS NOT NULL OR left_forearm_cm IS NOT NULL OR"
|
|
" right_forearm_cm IS NOT NULL OR left_thigh_cm IS NOT NULL OR"
|
|
" right_thigh_cm IS NOT NULL OR left_calf_cm IS NOT NULL OR"
|
|
" right_calf_cm IS NOT NULL"
|
|
" )"
|
|
");"
|
|
|
|
"PRAGMA user_version = 7;"
|
|
"COMMIT;";
|
|
|
|
static const char *const MIGRATE_V5_TO_V6_SQL =
|
|
"BEGIN IMMEDIATE;"
|
|
"ALTER TABLE session_exercises ADD COLUMN equipment_id TEXT;"
|
|
"PRAGMA user_version = 6;"
|
|
"COMMIT;";
|
|
|
|
/* WHY: a catalogue exercise can occur twice in one completed session. v7
|
|
* introduces a persistent occurrence ID and removes the invalid uniqueness
|
|
* constraint while retaining primary keys and all dependent measurements. */
|
|
static const char *const MIGRATE_V6_TO_V7_SQL =
|
|
"PRAGMA foreign_keys = OFF;"
|
|
"BEGIN IMMEDIATE;"
|
|
"ALTER TABLE session_exercises RENAME TO session_exercises_v6;"
|
|
"CREATE TABLE session_exercises ("
|
|
"id INTEGER PRIMARY KEY, entry_id TEXT NOT NULL UNIQUE,"
|
|
"session_row_id INTEGER NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,"
|
|
"exercise_row_id INTEGER NOT NULL REFERENCES exercises(id) ON DELETE RESTRICT,"
|
|
"recording_mode TEXT NOT NULL, data_fields INTEGER NOT NULL,"
|
|
"position INTEGER NOT NULL, load_mode TEXT NOT NULL, rest_seconds INTEGER NOT NULL,"
|
|
"target_sets INTEGER, target_reps INTEGER, target_duration_seconds INTEGER,"
|
|
"target_weight_kg REAL, equipment_id TEXT, notes TEXT, UNIQUE(session_row_id,position));"
|
|
"INSERT INTO session_exercises(id,entry_id,session_row_id,exercise_row_id,recording_mode,data_fields,position,load_mode,rest_seconds,target_sets,target_reps,target_duration_seconds,target_weight_kg,equipment_id,notes) "
|
|
"SELECT id,'sxe_legacy_' || id,session_row_id,exercise_row_id,recording_mode,data_fields,position,load_mode,rest_seconds,target_sets,target_reps,target_duration_seconds,target_weight_kg,equipment_id,notes FROM session_exercises_v6;"
|
|
"ALTER TABLE performed_sets RENAME TO performed_sets_v6;"
|
|
"CREATE TABLE performed_sets (id INTEGER PRIMARY KEY,session_exercise_row_id INTEGER NOT NULL REFERENCES session_exercises(id) ON DELETE CASCADE,position INTEGER NOT NULL,reps INTEGER,duration_seconds INTEGER,weight_kg REAL,UNIQUE(session_exercise_row_id,position));"
|
|
"INSERT INTO performed_sets SELECT * FROM performed_sets_v6;"
|
|
"ALTER TABLE continuous_activity RENAME TO continuous_activity_v6;"
|
|
"CREATE TABLE continuous_activity (id INTEGER PRIMARY KEY,session_exercise_row_id INTEGER NOT NULL UNIQUE REFERENCES session_exercises(id) ON DELETE CASCADE,duration_seconds INTEGER NOT NULL,speed_kmh REAL,distance_km REAL);"
|
|
"INSERT INTO continuous_activity SELECT * FROM continuous_activity_v6;"
|
|
"DROP TABLE performed_sets_v6;DROP TABLE continuous_activity_v6;DROP TABLE session_exercises_v6;"
|
|
"PRAGMA user_version = 7;COMMIT;PRAGMA foreign_keys = ON;";
|
|
|
|
/* CONTRACT: v8 stores only user-created equipment definitions. Canonical
|
|
* equipment remains generated from the frozen manifest; historic IDs are not
|
|
* backfilled because an occurrence reference has no trustworthy metadata. */
|
|
static const char *const MIGRATE_V7_TO_V8_SQL =
|
|
"BEGIN IMMEDIATE;"
|
|
"CREATE TABLE custom_equipment ("
|
|
"equipment_id TEXT PRIMARY KEY,display_name TEXT NOT NULL,"
|
|
"label_name TEXT NOT NULL,equipment_type TEXT NOT NULL,"
|
|
"load_semantics TEXT NOT NULL CHECK(load_semantics IN ('none','external','assistance'))"
|
|
");PRAGMA user_version = 8;COMMIT;";
|
|
|
|
static const char *const MIGRATE_V1_TO_V3_SQL =
|
|
"BEGIN IMMEDIATE;"
|
|
"ALTER TABLE sessions "
|
|
"ADD COLUMN session_type TEXT NOT NULL DEFAULT 'training' "
|
|
"CHECK (session_type IN ('training', 'max_test'));"
|
|
"ALTER TABLE exercises "
|
|
"ADD COLUMN recording_mode TEXT NOT NULL DEFAULT 'sets' "
|
|
"CHECK (recording_mode IN ('sets', 'continuous'));"
|
|
"ALTER TABLE exercises "
|
|
"ADD COLUMN data_fields INTEGER NOT NULL DEFAULT 0 "
|
|
"CHECK (data_fields >= 0 AND (data_fields & ~3) = 0);"
|
|
"ALTER TABLE session_exercises "
|
|
"ADD COLUMN recording_mode TEXT NOT NULL DEFAULT 'sets' "
|
|
"CHECK (recording_mode IN ('sets', 'continuous'));"
|
|
"ALTER TABLE session_exercises "
|
|
"ADD COLUMN data_fields INTEGER NOT NULL DEFAULT 0 "
|
|
"CHECK (data_fields >= 0 AND (data_fields & ~3) = 0);"
|
|
"CREATE TABLE continuous_activity ("
|
|
"id INTEGER PRIMARY KEY,"
|
|
"session_exercise_row_id INTEGER NOT NULL UNIQUE "
|
|
"REFERENCES session_exercises(id) ON DELETE CASCADE,"
|
|
"duration_seconds INTEGER NOT NULL CHECK (duration_seconds > 0),"
|
|
"speed_kmh REAL CHECK (speed_kmh > 0.0),"
|
|
"distance_km REAL CHECK (distance_km > 0.0)"
|
|
");"
|
|
"PRAGMA user_version = 3;"
|
|
"COMMIT;";
|
|
|
|
static const char *const MIGRATE_V2_TO_V3_SQL =
|
|
"BEGIN IMMEDIATE;"
|
|
"ALTER TABLE exercises "
|
|
"ADD COLUMN recording_mode TEXT NOT NULL DEFAULT 'sets' "
|
|
"CHECK (recording_mode IN ('sets', 'continuous'));"
|
|
"ALTER TABLE exercises "
|
|
"ADD COLUMN data_fields INTEGER NOT NULL DEFAULT 0 "
|
|
"CHECK (data_fields >= 0 AND (data_fields & ~3) = 0);"
|
|
"ALTER TABLE session_exercises "
|
|
"ADD COLUMN recording_mode TEXT NOT NULL DEFAULT 'sets' "
|
|
"CHECK (recording_mode IN ('sets', 'continuous'));"
|
|
"ALTER TABLE session_exercises "
|
|
"ADD COLUMN data_fields INTEGER NOT NULL DEFAULT 0 "
|
|
"CHECK (data_fields >= 0 AND (data_fields & ~3) = 0);"
|
|
"CREATE TABLE continuous_activity ("
|
|
"id INTEGER PRIMARY KEY,"
|
|
"session_exercise_row_id INTEGER NOT NULL UNIQUE "
|
|
"REFERENCES session_exercises(id) ON DELETE CASCADE,"
|
|
"duration_seconds INTEGER NOT NULL CHECK (duration_seconds > 0),"
|
|
"speed_kmh REAL CHECK (speed_kmh > 0.0),"
|
|
"distance_km REAL CHECK (distance_km > 0.0)"
|
|
");"
|
|
"PRAGMA user_version = 3;"
|
|
"COMMIT;";
|
|
|
|
static const char *const MIGRATE_V3_TO_V4_SQL_A =
|
|
"BEGIN IMMEDIATE;"
|
|
|
|
"CREATE TABLE session_exercises_v4 ("
|
|
" id INTEGER PRIMARY KEY,"
|
|
" session_row_id INTEGER NOT NULL"
|
|
" REFERENCES sessions(id) ON DELETE CASCADE,"
|
|
" exercise_row_id INTEGER NOT NULL"
|
|
" REFERENCES exercises(id) ON DELETE RESTRICT,"
|
|
" recording_mode TEXT NOT NULL"
|
|
" CHECK (recording_mode IN ('sets', 'continuous')),"
|
|
" data_fields INTEGER NOT NULL DEFAULT 0"
|
|
" CHECK (data_fields >= 0 AND (data_fields & ~3) = 0),"
|
|
" position INTEGER NOT NULL CHECK (position >= 0),"
|
|
" load_mode TEXT NOT NULL"
|
|
" CHECK (load_mode IN ('none', 'external', 'assistance')),"
|
|
" rest_seconds INTEGER NOT NULL CHECK (rest_seconds >= 0),"
|
|
" target_sets INTEGER CHECK (target_sets > 0),"
|
|
" target_reps INTEGER CHECK (target_reps >= 1),"
|
|
" target_duration_seconds INTEGER"
|
|
" CHECK (target_duration_seconds > 0),"
|
|
" target_weight_kg REAL CHECK (target_weight_kg > 0.0),"
|
|
" notes TEXT,"
|
|
" UNIQUE (session_row_id, position),"
|
|
" UNIQUE (session_row_id, exercise_row_id),"
|
|
" CHECK ("
|
|
" (recording_mode = 'sets' AND"
|
|
" target_sets IS NOT NULL AND"
|
|
" ((target_reps IS NOT NULL AND"
|
|
" target_duration_seconds IS NULL) OR"
|
|
" (target_reps IS NULL AND"
|
|
" target_duration_seconds IS NOT NULL))) OR"
|
|
" (recording_mode = 'continuous' AND"
|
|
" target_sets IS NULL AND"
|
|
" target_reps IS NULL AND"
|
|
" target_duration_seconds IS NULL AND"
|
|
" load_mode = 'none' AND"
|
|
" rest_seconds = 0 AND"
|
|
" target_weight_kg IS NULL)"
|
|
" ),"
|
|
" CHECK ("
|
|
" (load_mode = 'none' AND target_weight_kg IS NULL) OR"
|
|
" (load_mode IN ('external', 'assistance') AND"
|
|
" target_weight_kg IS NOT NULL)"
|
|
" )"
|
|
");"
|
|
|
|
"INSERT INTO session_exercises_v4("
|
|
"id, session_row_id, exercise_row_id, recording_mode, data_fields,"
|
|
"position, load_mode, rest_seconds, target_sets, target_reps,"
|
|
"target_duration_seconds, target_weight_kg, notes"
|
|
") SELECT "
|
|
"id, session_row_id, exercise_row_id, recording_mode, data_fields,"
|
|
"position, load_mode, rest_seconds, target_sets, target_reps,"
|
|
"target_duration_seconds, target_weight_kg, notes "
|
|
"FROM session_exercises;"
|
|
|
|
"CREATE TABLE performed_sets_v4 ("
|
|
" id INTEGER PRIMARY KEY,"
|
|
" session_exercise_row_id INTEGER NOT NULL"
|
|
" REFERENCES session_exercises_v4(id) ON DELETE CASCADE,"
|
|
" position INTEGER NOT NULL CHECK (position >= 0),"
|
|
" reps INTEGER CHECK (reps >= 0),"
|
|
" duration_seconds INTEGER CHECK (duration_seconds > 0),"
|
|
" weight_kg REAL CHECK (weight_kg > 0.0),"
|
|
" UNIQUE (session_exercise_row_id, position),"
|
|
" CHECK ("
|
|
" (reps IS NOT NULL AND duration_seconds IS NULL) OR"
|
|
" (reps IS NULL AND duration_seconds IS NOT NULL)"
|
|
" )"
|
|
");"
|
|
|
|
"INSERT INTO performed_sets_v4("
|
|
"id, session_exercise_row_id, position, reps,"
|
|
"duration_seconds, weight_kg"
|
|
") SELECT "
|
|
"id, session_exercise_row_id, position, reps,"
|
|
"duration_seconds, weight_kg "
|
|
"FROM performed_sets;";
|
|
|
|
static const char *const MIGRATE_V3_TO_V4_SQL_B =
|
|
"CREATE TABLE continuous_activity_v4 ("
|
|
" id INTEGER PRIMARY KEY,"
|
|
" session_exercise_row_id INTEGER NOT NULL UNIQUE"
|
|
" REFERENCES session_exercises_v4(id) ON DELETE CASCADE,"
|
|
" duration_seconds INTEGER NOT NULL CHECK (duration_seconds > 0),"
|
|
" speed_kmh REAL CHECK (speed_kmh > 0.0),"
|
|
" distance_km REAL CHECK (distance_km > 0.0)"
|
|
");"
|
|
|
|
"INSERT INTO continuous_activity_v4("
|
|
"id, session_exercise_row_id, duration_seconds,"
|
|
"speed_kmh, distance_km"
|
|
") SELECT "
|
|
"id, session_exercise_row_id, duration_seconds,"
|
|
"speed_kmh, distance_km "
|
|
"FROM continuous_activity;"
|
|
|
|
"DROP TABLE continuous_activity;"
|
|
"DROP TABLE performed_sets;"
|
|
"DROP TABLE session_exercises;"
|
|
|
|
"ALTER TABLE session_exercises_v4"
|
|
" RENAME TO session_exercises;"
|
|
"ALTER TABLE performed_sets_v4"
|
|
" RENAME TO performed_sets;"
|
|
"ALTER TABLE continuous_activity_v4"
|
|
" RENAME TO continuous_activity;"
|
|
|
|
"PRAGMA user_version = 4;"
|
|
"COMMIT;";
|
|
|
|
static const char *const MIGRATE_V4_TO_V5_SQL_A =
|
|
"BEGIN IMMEDIATE;"
|
|
|
|
"CREATE TABLE session_exercises_v5 ("
|
|
" id INTEGER PRIMARY KEY,"
|
|
" session_row_id INTEGER NOT NULL"
|
|
" REFERENCES sessions(id) ON DELETE CASCADE,"
|
|
" exercise_row_id INTEGER NOT NULL"
|
|
" REFERENCES exercises(id) ON DELETE RESTRICT,"
|
|
" recording_mode TEXT NOT NULL DEFAULT 'sets'"
|
|
" CHECK (recording_mode IN ('sets', 'continuous')),"
|
|
" data_fields INTEGER NOT NULL DEFAULT 0"
|
|
" CHECK (data_fields >= 0 AND (data_fields & ~3) = 0),"
|
|
" position INTEGER NOT NULL CHECK (position >= 0),"
|
|
" load_mode TEXT NOT NULL"
|
|
" CHECK (load_mode IN ('none', 'external', 'assistance')),"
|
|
" rest_seconds INTEGER NOT NULL CHECK (rest_seconds >= 0),"
|
|
" target_sets INTEGER CHECK (target_sets > 0),"
|
|
" target_reps INTEGER CHECK (target_reps >= 1),"
|
|
" target_duration_seconds INTEGER"
|
|
" CHECK (target_duration_seconds > 0),"
|
|
" target_weight_kg REAL CHECK (target_weight_kg > 0.0),"
|
|
" notes TEXT,"
|
|
" UNIQUE (session_row_id, position),"
|
|
" UNIQUE (session_row_id, exercise_row_id),"
|
|
" CHECK ("
|
|
" (recording_mode = 'sets' AND"
|
|
" ("
|
|
" (target_sets IS NULL AND"
|
|
" target_reps IS NULL AND"
|
|
" target_duration_seconds IS NULL) OR"
|
|
" (target_sets IS NOT NULL AND"
|
|
" ((target_reps IS NOT NULL AND"
|
|
" target_duration_seconds IS NULL) OR"
|
|
" (target_reps IS NULL AND"
|
|
" target_duration_seconds IS NOT NULL)))"
|
|
" )) OR"
|
|
" (recording_mode = 'continuous' AND"
|
|
" target_sets IS NULL AND"
|
|
" target_reps IS NULL AND"
|
|
" target_duration_seconds IS NULL AND"
|
|
" load_mode = 'none' AND"
|
|
" rest_seconds = 0 AND"
|
|
" target_weight_kg IS NULL)"
|
|
" ),"
|
|
" CHECK ("
|
|
" (load_mode = 'none' AND target_weight_kg IS NULL) OR"
|
|
" (load_mode IN ('external', 'assistance') AND"
|
|
" target_weight_kg IS NOT NULL)"
|
|
" )"
|
|
");"
|
|
|
|
"INSERT INTO session_exercises_v5("
|
|
"id, session_row_id, exercise_row_id, recording_mode, data_fields,"
|
|
"position, load_mode, rest_seconds, target_sets, target_reps,"
|
|
"target_duration_seconds, target_weight_kg, notes"
|
|
") SELECT "
|
|
"id, session_row_id, exercise_row_id, recording_mode, data_fields,"
|
|
"position, load_mode, rest_seconds, target_sets, target_reps,"
|
|
"target_duration_seconds, target_weight_kg, notes "
|
|
"FROM session_exercises;"
|
|
|
|
"CREATE TABLE performed_sets_v5 ("
|
|
" id INTEGER PRIMARY KEY,"
|
|
" session_exercise_row_id INTEGER NOT NULL"
|
|
" REFERENCES session_exercises_v5(id) ON DELETE CASCADE,"
|
|
" position INTEGER NOT NULL CHECK (position >= 0),"
|
|
" reps INTEGER CHECK (reps >= 0),"
|
|
" duration_seconds INTEGER CHECK (duration_seconds > 0),"
|
|
" weight_kg REAL CHECK (weight_kg > 0.0),"
|
|
" UNIQUE (session_exercise_row_id, position),"
|
|
" CHECK ("
|
|
" (reps IS NOT NULL AND duration_seconds IS NULL) OR"
|
|
" (reps IS NULL AND duration_seconds IS NOT NULL)"
|
|
" )"
|
|
");"
|
|
|
|
"INSERT INTO performed_sets_v5("
|
|
"id, session_exercise_row_id, position, reps,"
|
|
"duration_seconds, weight_kg"
|
|
") SELECT "
|
|
"id, session_exercise_row_id, position, reps,"
|
|
"duration_seconds, weight_kg "
|
|
"FROM performed_sets;";
|
|
|
|
static const char *const MIGRATE_V4_TO_V5_SQL_B =
|
|
"CREATE TABLE continuous_activity_v5 ("
|
|
" id INTEGER PRIMARY KEY,"
|
|
" session_exercise_row_id INTEGER NOT NULL UNIQUE"
|
|
" REFERENCES session_exercises_v5(id) ON DELETE CASCADE,"
|
|
" duration_seconds INTEGER NOT NULL CHECK (duration_seconds > 0),"
|
|
" speed_kmh REAL CHECK (speed_kmh > 0.0),"
|
|
" distance_km REAL CHECK (distance_km > 0.0)"
|
|
");"
|
|
|
|
"INSERT INTO continuous_activity_v5("
|
|
"id, session_exercise_row_id, duration_seconds,"
|
|
"speed_kmh, distance_km"
|
|
") SELECT "
|
|
"id, session_exercise_row_id, duration_seconds,"
|
|
"speed_kmh, distance_km "
|
|
"FROM continuous_activity;"
|
|
|
|
"DROP TABLE continuous_activity;"
|
|
"DROP TABLE performed_sets;"
|
|
"DROP TABLE session_exercises;"
|
|
|
|
"ALTER TABLE session_exercises_v5"
|
|
" RENAME TO session_exercises;"
|
|
"ALTER TABLE performed_sets_v5"
|
|
" RENAME TO performed_sets;"
|
|
"ALTER TABLE continuous_activity_v5"
|
|
" RENAME TO continuous_activity;"
|
|
|
|
"PRAGMA user_version = 5;"
|
|
"COMMIT;";
|
|
|
|
static TrainlogStatus execute_sql(
|
|
TrainlogDatabase *database,
|
|
const char *sql
|
|
)
|
|
{
|
|
if (database == NULL || database->connection == NULL || sql == NULL) {
|
|
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
}
|
|
|
|
if (sqlite3_exec(database->connection, sql, NULL, NULL, NULL) != SQLITE_OK) {
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
return TRAINLOG_STATUS_OK;
|
|
}
|
|
|
|
static TrainlogStatus read_single_int_pragma(
|
|
TrainlogDatabase *database,
|
|
const char *sql,
|
|
int *output
|
|
)
|
|
{
|
|
sqlite3_stmt *statement = NULL;
|
|
int rc;
|
|
|
|
if (database == NULL ||
|
|
database->connection == NULL ||
|
|
sql == NULL ||
|
|
output == NULL) {
|
|
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
}
|
|
|
|
rc = sqlite3_prepare_v2(database->connection, sql, -1, &statement, NULL);
|
|
if (rc != SQLITE_OK) {
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
rc = sqlite3_step(statement);
|
|
if (rc != SQLITE_ROW) {
|
|
(void)sqlite3_finalize(statement);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
*output = sqlite3_column_int(statement, 0);
|
|
|
|
rc = sqlite3_finalize(statement);
|
|
return rc == SQLITE_OK
|
|
? TRAINLOG_STATUS_OK
|
|
: TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
static TrainlogStatus initialize_or_validate_schema(
|
|
TrainlogDatabase *database,
|
|
char *output_diagnostic,
|
|
size_t output_diagnostic_capacity
|
|
)
|
|
{
|
|
int version = 0;
|
|
TrainlogStatus status;
|
|
|
|
status =
|
|
trainlog_database_schema_version(
|
|
database,
|
|
&version
|
|
);
|
|
|
|
if (
|
|
status !=
|
|
TRAINLOG_STATUS_OK
|
|
) {
|
|
set_open_diagnostic(
|
|
output_diagnostic,
|
|
output_diagnostic_capacity,
|
|
"read schema version",
|
|
database->connection,
|
|
SQLITE_ERROR
|
|
);
|
|
return status;
|
|
}
|
|
|
|
if (
|
|
version >
|
|
TRAINLOG_DATABASE_SCHEMA_VERSION
|
|
) {
|
|
char message[128];
|
|
(void)snprintf(
|
|
message, sizeof(message),
|
|
"schema version %d is newer than supported version %d",
|
|
version, TRAINLOG_DATABASE_SCHEMA_VERSION);
|
|
set_application_diagnostic(
|
|
output_diagnostic, output_diagnostic_capacity, message);
|
|
return
|
|
TRAINLOG_STATUS_SCHEMA_UNSUPPORTED;
|
|
}
|
|
|
|
if (
|
|
version ==
|
|
TRAINLOG_DATABASE_SCHEMA_VERSION
|
|
) {
|
|
return TRAINLOG_STATUS_OK;
|
|
}
|
|
|
|
if (version == 0) {
|
|
status =
|
|
execute_sql(
|
|
database,
|
|
SCHEMA_V7_SQL_A
|
|
);
|
|
|
|
if (
|
|
status ==
|
|
TRAINLOG_STATUS_OK
|
|
) {
|
|
status =
|
|
execute_sql(
|
|
database,
|
|
SCHEMA_V7_SQL_B
|
|
);
|
|
}
|
|
if (status == TRAINLOG_STATUS_OK) {
|
|
status = execute_sql(database, MIGRATE_V7_TO_V8_SQL);
|
|
}
|
|
} else if (version == 7) {
|
|
/* CONTRACT: v7 is the immediate historic schema and must open through
|
|
* its lossless custom-equipment-table migration. */
|
|
status = execute_sql(database, MIGRATE_V7_TO_V8_SQL);
|
|
} else {
|
|
if (version == 1) {
|
|
status =
|
|
execute_sql(
|
|
database,
|
|
MIGRATE_V1_TO_V3_SQL
|
|
);
|
|
|
|
if (
|
|
status ==
|
|
TRAINLOG_STATUS_OK
|
|
) {
|
|
status =
|
|
execute_sql(
|
|
database,
|
|
MIGRATE_V3_TO_V4_SQL_A
|
|
);
|
|
}
|
|
|
|
if (
|
|
status ==
|
|
TRAINLOG_STATUS_OK
|
|
) {
|
|
status =
|
|
execute_sql(
|
|
database,
|
|
MIGRATE_V3_TO_V4_SQL_B
|
|
);
|
|
}
|
|
} else if (version == 2) {
|
|
status =
|
|
execute_sql(
|
|
database,
|
|
MIGRATE_V2_TO_V3_SQL
|
|
);
|
|
|
|
if (
|
|
status ==
|
|
TRAINLOG_STATUS_OK
|
|
) {
|
|
status =
|
|
execute_sql(
|
|
database,
|
|
MIGRATE_V3_TO_V4_SQL_A
|
|
);
|
|
}
|
|
|
|
if (
|
|
status ==
|
|
TRAINLOG_STATUS_OK
|
|
) {
|
|
status =
|
|
execute_sql(
|
|
database,
|
|
MIGRATE_V3_TO_V4_SQL_B
|
|
);
|
|
}
|
|
} else if (version == 3) {
|
|
status =
|
|
execute_sql(
|
|
database,
|
|
MIGRATE_V3_TO_V4_SQL_A
|
|
);
|
|
|
|
if (
|
|
status ==
|
|
TRAINLOG_STATUS_OK
|
|
) {
|
|
status =
|
|
execute_sql(
|
|
database,
|
|
MIGRATE_V3_TO_V4_SQL_B
|
|
);
|
|
}
|
|
} else if (version == 4) {
|
|
status =
|
|
TRAINLOG_STATUS_OK;
|
|
} else if (version == 5) {
|
|
status = TRAINLOG_STATUS_OK;
|
|
} else if (version == 6) {
|
|
status = TRAINLOG_STATUS_OK;
|
|
} else {
|
|
char message[128];
|
|
(void)snprintf(
|
|
message, sizeof(message),
|
|
"schema version %d is unsupported (supported through version %d)",
|
|
version, TRAINLOG_DATABASE_SCHEMA_VERSION);
|
|
set_application_diagnostic(
|
|
output_diagnostic, output_diagnostic_capacity, message);
|
|
return
|
|
TRAINLOG_STATUS_SCHEMA_UNSUPPORTED;
|
|
}
|
|
|
|
if (
|
|
status ==
|
|
TRAINLOG_STATUS_OK
|
|
) {
|
|
status =
|
|
execute_sql(
|
|
database,
|
|
MIGRATE_V4_TO_V5_SQL_A
|
|
);
|
|
}
|
|
|
|
if (
|
|
status ==
|
|
TRAINLOG_STATUS_OK
|
|
) {
|
|
status =
|
|
execute_sql(
|
|
database,
|
|
MIGRATE_V4_TO_V5_SQL_B
|
|
);
|
|
}
|
|
|
|
if (status == TRAINLOG_STATUS_OK && version < 6) {
|
|
status = execute_sql(database, MIGRATE_V5_TO_V6_SQL);
|
|
}
|
|
if (status == TRAINLOG_STATUS_OK) {
|
|
status = execute_sql(database, MIGRATE_V6_TO_V7_SQL);
|
|
}
|
|
if (status == TRAINLOG_STATUS_OK) {
|
|
status = execute_sql(database, MIGRATE_V7_TO_V8_SQL);
|
|
}
|
|
}
|
|
|
|
if (
|
|
status !=
|
|
TRAINLOG_STATUS_OK
|
|
) {
|
|
set_open_diagnostic(
|
|
output_diagnostic,
|
|
output_diagnostic_capacity,
|
|
version == 0 ? "create schema v8" : "migrate database to schema v8",
|
|
database->connection,
|
|
SQLITE_ERROR
|
|
);
|
|
(void)sqlite3_exec(
|
|
database->connection,
|
|
"ROLLBACK;",
|
|
NULL,
|
|
NULL,
|
|
NULL
|
|
);
|
|
}
|
|
|
|
return status;
|
|
}
|
|
|
|
TrainlogStatus trainlog_database_open(
|
|
const char *path,
|
|
TrainlogDatabase **output_database
|
|
)
|
|
{
|
|
return trainlog_database_open_with_diagnostic(
|
|
path,
|
|
output_database,
|
|
NULL,
|
|
0U
|
|
);
|
|
}
|
|
|
|
TrainlogStatus trainlog_database_open_with_diagnostic(
|
|
const char *path,
|
|
TrainlogDatabase **output_database,
|
|
char *output_diagnostic,
|
|
size_t output_diagnostic_capacity
|
|
)
|
|
{
|
|
TrainlogDatabase *database;
|
|
int rc;
|
|
TrainlogStatus status;
|
|
|
|
if (path == NULL || path[0] == '\0' || output_database == NULL) {
|
|
set_open_diagnostic(
|
|
output_diagnostic,
|
|
output_diagnostic_capacity,
|
|
"validate database path",
|
|
NULL,
|
|
SQLITE_MISUSE
|
|
);
|
|
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
}
|
|
|
|
*output_database = NULL;
|
|
if (output_diagnostic != NULL && output_diagnostic_capacity > 0U) {
|
|
output_diagnostic[0] = '\0';
|
|
}
|
|
|
|
database = calloc(1U, sizeof(*database));
|
|
if (database == NULL) {
|
|
set_open_diagnostic(
|
|
output_diagnostic,
|
|
output_diagnostic_capacity,
|
|
"allocate database handle",
|
|
NULL,
|
|
SQLITE_NOMEM
|
|
);
|
|
return TRAINLOG_STATUS_SYSTEM_ERROR;
|
|
}
|
|
|
|
rc = sqlite3_open_v2(
|
|
path,
|
|
&database->connection,
|
|
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX,
|
|
NULL
|
|
);
|
|
if (rc != SQLITE_OK) {
|
|
set_open_diagnostic(
|
|
output_diagnostic,
|
|
output_diagnostic_capacity,
|
|
"open database",
|
|
database->connection,
|
|
rc
|
|
);
|
|
trainlog_database_close(database);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
if (sqlite3_busy_timeout(database->connection, 5000) != SQLITE_OK) {
|
|
set_open_diagnostic(
|
|
output_diagnostic,
|
|
output_diagnostic_capacity,
|
|
"configure database busy timeout",
|
|
database->connection,
|
|
SQLITE_ERROR
|
|
);
|
|
trainlog_database_close(database);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
status = execute_sql(database, "PRAGMA foreign_keys = ON;");
|
|
if (status != TRAINLOG_STATUS_OK) {
|
|
set_open_diagnostic(
|
|
output_diagnostic,
|
|
output_diagnostic_capacity,
|
|
"enable foreign-key enforcement",
|
|
database->connection,
|
|
SQLITE_ERROR
|
|
);
|
|
trainlog_database_close(database);
|
|
return status;
|
|
}
|
|
|
|
status = initialize_or_validate_schema(
|
|
database,
|
|
output_diagnostic,
|
|
output_diagnostic_capacity
|
|
);
|
|
if (status != TRAINLOG_STATUS_OK) {
|
|
trainlog_database_close(database);
|
|
return status;
|
|
}
|
|
|
|
*output_database = database;
|
|
return TRAINLOG_STATUS_OK;
|
|
}
|
|
|
|
void trainlog_database_close(TrainlogDatabase *database)
|
|
{
|
|
if (database == NULL) {
|
|
return;
|
|
}
|
|
|
|
if (database->connection != NULL) {
|
|
(void)sqlite3_close(database->connection);
|
|
}
|
|
|
|
free(database);
|
|
}
|
|
|
|
TrainlogStatus trainlog_database_schema_version(
|
|
TrainlogDatabase *database,
|
|
int *output_version
|
|
)
|
|
{
|
|
return read_single_int_pragma(
|
|
database,
|
|
"PRAGMA user_version;",
|
|
output_version
|
|
);
|
|
}
|
|
|
|
TrainlogStatus trainlog_database_foreign_keys_enabled(
|
|
TrainlogDatabase *database,
|
|
int *output_enabled
|
|
)
|
|
{
|
|
return read_single_int_pragma(
|
|
database,
|
|
"PRAGMA foreign_keys;",
|
|
output_enabled
|
|
);
|
|
}
|
|
|
|
TrainlogStatus trainlog_database_create_custom_equipment(
|
|
TrainlogDatabase *database,
|
|
const TrainlogCustomEquipment *equipment
|
|
)
|
|
{
|
|
sqlite3_stmt *statement = NULL;
|
|
int rc;
|
|
|
|
/* CONTRACT: custom IDs must not shadow frozen manifest identities. */
|
|
if (database == NULL || equipment == NULL || equipment->equipment_id[0] == '\0' ||
|
|
equipment->display_name[0] == '\0' || equipment->equipment_type[0] == '\0' ||
|
|
trainlog_equipment_catalog_lookup(equipment->equipment_id) != NULL ||
|
|
(strcmp(equipment->load_semantics, "none") != 0 &&
|
|
strcmp(equipment->load_semantics, "external") != 0 &&
|
|
strcmp(equipment->load_semantics, "assistance") != 0)) {
|
|
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
}
|
|
rc = sqlite3_prepare_v2(database->connection,
|
|
"INSERT INTO custom_equipment(equipment_id,display_name,label_name,equipment_type,load_semantics) VALUES(?1,?2,?3,?4,?5);",
|
|
-1, &statement, NULL);
|
|
if (rc != SQLITE_OK) return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
(void)sqlite3_bind_text(statement, 1, equipment->equipment_id, -1, SQLITE_TRANSIENT);
|
|
(void)sqlite3_bind_text(statement, 2, equipment->display_name, -1, SQLITE_TRANSIENT);
|
|
(void)sqlite3_bind_text(statement, 3, equipment->label_name, -1, SQLITE_TRANSIENT);
|
|
(void)sqlite3_bind_text(statement, 4, equipment->equipment_type, -1, SQLITE_TRANSIENT);
|
|
(void)sqlite3_bind_text(statement, 5, equipment->load_semantics, -1, SQLITE_TRANSIENT);
|
|
rc = sqlite3_step(statement);
|
|
(void)sqlite3_finalize(statement);
|
|
return rc == SQLITE_DONE ? TRAINLOG_STATUS_OK : TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
TrainlogStatus trainlog_database_list_custom_equipment(
|
|
TrainlogDatabase *database, TrainlogCustomEquipment *output,
|
|
size_t capacity, size_t *output_count
|
|
)
|
|
{
|
|
sqlite3_stmt *statement = NULL;
|
|
size_t count = 0U;
|
|
int rc;
|
|
if (database == NULL || output_count == NULL || (capacity > 0U && output == NULL))
|
|
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
*output_count = 0U;
|
|
rc = sqlite3_prepare_v2(database->connection,
|
|
"SELECT equipment_id,display_name,label_name,equipment_type,load_semantics FROM custom_equipment ORDER BY display_name COLLATE NOCASE,equipment_id;",
|
|
-1, &statement, NULL);
|
|
if (rc != SQLITE_OK) return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
while ((rc = sqlite3_step(statement)) == SQLITE_ROW) {
|
|
if (count < capacity) {
|
|
TrainlogCustomEquipment *item = &output[count];
|
|
(void)memset(item, 0, sizeof(*item));
|
|
(void)snprintf(item->equipment_id, sizeof(item->equipment_id), "%s", sqlite3_column_text(statement, 0));
|
|
(void)snprintf(item->display_name, sizeof(item->display_name), "%s", sqlite3_column_text(statement, 1));
|
|
(void)snprintf(item->label_name, sizeof(item->label_name), "%s", sqlite3_column_text(statement, 2));
|
|
(void)snprintf(item->equipment_type, sizeof(item->equipment_type), "%s", sqlite3_column_text(statement, 3));
|
|
(void)snprintf(item->load_semantics, sizeof(item->load_semantics), "%s", sqlite3_column_text(statement, 4));
|
|
}
|
|
++count;
|
|
}
|
|
(void)sqlite3_finalize(statement);
|
|
*output_count = count;
|
|
return rc == SQLITE_DONE ? TRAINLOG_STATUS_OK : TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
TrainlogStatus trainlog_database_resolve_equipment(
|
|
TrainlogDatabase *database, const char *equipment_id,
|
|
TrainlogResolvedEquipment *output
|
|
)
|
|
{
|
|
const TrainlogEquipment *supplied;
|
|
sqlite3_stmt *statement = NULL;
|
|
int rc;
|
|
if (database == NULL || equipment_id == NULL || equipment_id[0] == '\0' ||
|
|
output == NULL) return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
(void)memset(output, 0, sizeof(*output));
|
|
(void)snprintf(output->equipment_id, sizeof(output->equipment_id), "%s", equipment_id);
|
|
supplied = trainlog_equipment_catalog_lookup(equipment_id);
|
|
if (supplied != NULL) {
|
|
(void)snprintf(output->display_name, sizeof(output->display_name), "%s", supplied->display_name);
|
|
(void)snprintf(output->label_name, sizeof(output->label_name), "%s", supplied->label_name);
|
|
(void)snprintf(output->equipment_type, sizeof(output->equipment_type), "%s", supplied->equipment_type);
|
|
(void)snprintf(output->load_semantics, sizeof(output->load_semantics), "%s", supplied->load_semantics);
|
|
output->origin = TRAINLOG_EQUIPMENT_SUPPLIED;
|
|
return TRAINLOG_STATUS_OK;
|
|
}
|
|
rc = sqlite3_prepare_v2(database->connection,
|
|
"SELECT display_name,label_name,equipment_type,load_semantics FROM custom_equipment WHERE equipment_id=?1;",
|
|
-1, &statement, NULL);
|
|
if (rc != SQLITE_OK) return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
(void)sqlite3_bind_text(statement, 1, equipment_id, -1, SQLITE_TRANSIENT);
|
|
rc = sqlite3_step(statement);
|
|
if (rc == SQLITE_ROW) {
|
|
(void)snprintf(output->display_name, sizeof(output->display_name), "%s", sqlite3_column_text(statement, 0));
|
|
(void)snprintf(output->label_name, sizeof(output->label_name), "%s", sqlite3_column_text(statement, 1));
|
|
(void)snprintf(output->equipment_type, sizeof(output->equipment_type), "%s", sqlite3_column_text(statement, 2));
|
|
(void)snprintf(output->load_semantics, sizeof(output->load_semantics), "%s", sqlite3_column_text(statement, 3));
|
|
output->origin = TRAINLOG_EQUIPMENT_CUSTOM;
|
|
} else if (rc == SQLITE_DONE) {
|
|
/* INVARIANT: historic references survive catalogue changes visibly. */
|
|
(void)snprintf(output->display_name, sizeof(output->display_name), "Inconnu (%s)", equipment_id);
|
|
output->origin = TRAINLOG_EQUIPMENT_UNKNOWN;
|
|
rc = SQLITE_ROW;
|
|
}
|
|
(void)sqlite3_finalize(statement);
|
|
return rc == SQLITE_ROW ? TRAINLOG_STATUS_OK : TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
TrainlogStatus trainlog_database_list_exercise_equipment(
|
|
TrainlogDatabase *database, const char *exercise_id,
|
|
TrainlogResolvedEquipment *output, size_t capacity, size_t *output_count
|
|
)
|
|
{
|
|
sqlite3_stmt *statement = NULL;
|
|
size_t count = 0U;
|
|
int rc;
|
|
if (database == NULL || exercise_id == NULL || output_count == NULL ||
|
|
(capacity > 0U && output == NULL)) return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
*output_count = 0U;
|
|
rc = sqlite3_prepare_v2(database->connection,
|
|
"SELECT DISTINCT se.equipment_id FROM session_exercises se JOIN exercises e ON e.id=se.exercise_row_id WHERE e.exercise_id=?1 AND se.equipment_id IS NOT NULL AND se.equipment_id<>'' ORDER BY se.equipment_id;",
|
|
-1, &statement, NULL);
|
|
if (rc != SQLITE_OK) return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
(void)sqlite3_bind_text(statement, 1, exercise_id, -1, SQLITE_TRANSIENT);
|
|
while ((rc = sqlite3_step(statement)) == SQLITE_ROW) {
|
|
const char *id = (const char *)sqlite3_column_text(statement, 0);
|
|
if (count < capacity && trainlog_database_resolve_equipment(database, id, &output[count]) != TRAINLOG_STATUS_OK) {
|
|
(void)sqlite3_finalize(statement);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
++count;
|
|
}
|
|
(void)sqlite3_finalize(statement);
|
|
*output_count = count;
|
|
return rc == SQLITE_DONE ? TRAINLOG_STATUS_OK : TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
TrainlogStatus trainlog_database_begin(TrainlogDatabase *database)
|
|
{
|
|
return execute_sql(database, "BEGIN IMMEDIATE;");
|
|
}
|
|
|
|
TrainlogStatus trainlog_database_commit(TrainlogDatabase *database)
|
|
{
|
|
return execute_sql(database, "COMMIT;");
|
|
}
|
|
|
|
TrainlogStatus trainlog_database_rollback(TrainlogDatabase *database)
|
|
{
|
|
return execute_sql(database, "ROLLBACK;");
|
|
}
|
|
|
|
static const char *tracking_mode_to_sql(TrainlogTrackingMode mode)
|
|
{
|
|
switch (mode) {
|
|
case TRAINLOG_TRACKING_REPS:
|
|
return "reps";
|
|
case TRAINLOG_TRACKING_DURATION:
|
|
return "duration";
|
|
default:
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
static const char *recording_mode_to_sql(
|
|
TrainlogRecordingMode mode
|
|
)
|
|
{
|
|
switch (mode) {
|
|
case TRAINLOG_RECORDING_SETS:
|
|
return "sets";
|
|
case TRAINLOG_RECORDING_CONTINUOUS:
|
|
return "continuous";
|
|
default:
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
static bool exercise_profile_valid(
|
|
TrainlogTrackingMode tracking_mode,
|
|
TrainlogRecordingMode recording_mode,
|
|
TrainlogExerciseDataFields data_fields
|
|
)
|
|
{
|
|
if ((data_fields &
|
|
~TRAINLOG_EXERCISE_DATA_KNOWN_MASK) != 0U) {
|
|
return false;
|
|
}
|
|
|
|
if (recording_mode == TRAINLOG_RECORDING_CONTINUOUS) {
|
|
return tracking_mode == TRAINLOG_TRACKING_DURATION;
|
|
}
|
|
|
|
return
|
|
recording_mode == TRAINLOG_RECORDING_SETS &&
|
|
(tracking_mode == TRAINLOG_TRACKING_REPS ||
|
|
tracking_mode == TRAINLOG_TRACKING_DURATION);
|
|
}
|
|
|
|
|
|
static const char *load_mode_to_sql(TrainlogLoadMode mode)
|
|
{
|
|
switch (mode) {
|
|
case TRAINLOG_LOAD_NONE:
|
|
return "none";
|
|
case TRAINLOG_LOAD_EXTERNAL:
|
|
return "external";
|
|
case TRAINLOG_LOAD_ASSISTANCE:
|
|
return "assistance";
|
|
default:
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
static const char *session_type_to_sql(
|
|
TrainlogSessionType type
|
|
)
|
|
{
|
|
switch (type) {
|
|
case TRAINLOG_SESSION_TRAINING:
|
|
return "training";
|
|
|
|
case TRAINLOG_SESSION_MAX_TEST:
|
|
return "max_test";
|
|
|
|
default:
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
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
|
|
)
|
|
{
|
|
static const char *const SQL =
|
|
"INSERT INTO exercises("
|
|
"exercise_id, name, normalized_name, tracking_mode, "
|
|
"recording_mode, data_fields"
|
|
") VALUES(?1, ?2, ?3, ?4, ?5, ?6);";
|
|
|
|
sqlite3_stmt *statement = NULL;
|
|
const char *tracking;
|
|
const char *recording;
|
|
int rc;
|
|
|
|
if (database == NULL ||
|
|
database->connection == NULL ||
|
|
exercise_id == NULL ||
|
|
exercise_id[0] == '\0' ||
|
|
name == NULL ||
|
|
name[0] == '\0' ||
|
|
normalized_name == NULL ||
|
|
normalized_name[0] == '\0' ||
|
|
!exercise_profile_valid(
|
|
tracking_mode,
|
|
recording_mode,
|
|
data_fields
|
|
)) {
|
|
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
}
|
|
|
|
tracking = tracking_mode_to_sql(tracking_mode);
|
|
recording = recording_mode_to_sql(recording_mode);
|
|
|
|
if (tracking == NULL || recording == NULL) {
|
|
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
}
|
|
|
|
rc = sqlite3_prepare_v2(
|
|
database->connection,
|
|
SQL,
|
|
-1,
|
|
&statement,
|
|
NULL
|
|
);
|
|
|
|
if (rc != SQLITE_OK) {
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
if (sqlite3_bind_text(statement, 1, exercise_id, -1, SQLITE_TRANSIENT) != SQLITE_OK ||
|
|
sqlite3_bind_text(statement, 2, name, -1, SQLITE_TRANSIENT) != SQLITE_OK ||
|
|
sqlite3_bind_text(statement, 3, normalized_name, -1, SQLITE_TRANSIENT) != SQLITE_OK ||
|
|
sqlite3_bind_text(statement, 4, tracking, -1, SQLITE_STATIC) != SQLITE_OK ||
|
|
sqlite3_bind_text(statement, 5, recording, -1, SQLITE_STATIC) != SQLITE_OK ||
|
|
sqlite3_bind_int64(statement, 6, (sqlite3_int64)data_fields) != SQLITE_OK) {
|
|
(void)sqlite3_finalize(statement);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
rc = sqlite3_step(statement);
|
|
|
|
if (rc == SQLITE_CONSTRAINT) {
|
|
(void)sqlite3_finalize(statement);
|
|
return TRAINLOG_STATUS_CONFLICT;
|
|
}
|
|
|
|
if (rc != SQLITE_DONE) {
|
|
(void)sqlite3_finalize(statement);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
return sqlite3_finalize(statement) == SQLITE_OK
|
|
? TRAINLOG_STATUS_OK
|
|
: TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
TrainlogStatus trainlog_database_insert_exercise(
|
|
TrainlogDatabase *database,
|
|
const char *exercise_id,
|
|
const char *name,
|
|
const char *normalized_name,
|
|
TrainlogTrackingMode tracking_mode
|
|
)
|
|
{
|
|
return trainlog_database_insert_exercise_profiled(
|
|
database,
|
|
exercise_id,
|
|
name,
|
|
normalized_name,
|
|
tracking_mode,
|
|
TRAINLOG_RECORDING_SETS,
|
|
0U
|
|
);
|
|
}
|
|
|
|
static TrainlogStatus count_query(
|
|
TrainlogDatabase *database,
|
|
const char *sql,
|
|
size_t *output_count
|
|
)
|
|
{
|
|
sqlite3_stmt *statement = NULL;
|
|
sqlite3_int64 count;
|
|
int rc;
|
|
|
|
if (database == NULL ||
|
|
database->connection == NULL ||
|
|
sql == NULL ||
|
|
output_count == NULL) {
|
|
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
}
|
|
|
|
rc = sqlite3_prepare_v2(database->connection, sql, -1, &statement, NULL);
|
|
if (rc != SQLITE_OK) {
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
rc = sqlite3_step(statement);
|
|
if (rc != SQLITE_ROW) {
|
|
(void)sqlite3_finalize(statement);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
count = sqlite3_column_int64(statement, 0);
|
|
if (count < 0 || (uint64_t)count > (uint64_t)SIZE_MAX) {
|
|
(void)sqlite3_finalize(statement);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
*output_count = (size_t)count;
|
|
|
|
return sqlite3_finalize(statement) == SQLITE_OK
|
|
? TRAINLOG_STATUS_OK
|
|
: TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
TrainlogStatus trainlog_database_exercise_count(
|
|
TrainlogDatabase *database,
|
|
size_t *output_count
|
|
)
|
|
{
|
|
return count_query(
|
|
database,
|
|
"SELECT COUNT(*) FROM exercises;",
|
|
output_count
|
|
);
|
|
}
|
|
|
|
TrainlogStatus trainlog_database_session_count(
|
|
TrainlogDatabase *database,
|
|
size_t *output_count
|
|
)
|
|
{
|
|
return count_query(
|
|
database,
|
|
"SELECT COUNT(*) FROM sessions;",
|
|
output_count
|
|
);
|
|
}
|
|
|
|
static TrainlogTrackingMode tracking_mode_from_sql(const char *text)
|
|
{
|
|
return text != NULL && strcmp(text, "duration") == 0
|
|
? TRAINLOG_TRACKING_DURATION
|
|
: TRAINLOG_TRACKING_REPS;
|
|
}
|
|
|
|
static TrainlogRecordingMode recording_mode_from_sql(
|
|
const char *text
|
|
)
|
|
{
|
|
return
|
|
text != NULL &&
|
|
strcmp(text, "continuous") == 0
|
|
? TRAINLOG_RECORDING_CONTINUOUS
|
|
: TRAINLOG_RECORDING_SETS;
|
|
}
|
|
|
|
|
|
static bool session_type_from_sql(
|
|
const char *text,
|
|
TrainlogSessionType *output
|
|
)
|
|
{
|
|
if (text == NULL ||
|
|
output == NULL) {
|
|
return false;
|
|
}
|
|
|
|
if (strcmp(text, "training") == 0) {
|
|
*output = TRAINLOG_SESSION_TRAINING;
|
|
return true;
|
|
}
|
|
|
|
if (strcmp(text, "max_test") == 0) {
|
|
*output = TRAINLOG_SESSION_MAX_TEST;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
TrainlogStatus trainlog_database_list_exercises(
|
|
TrainlogDatabase *database,
|
|
TrainlogExercise *output,
|
|
size_t capacity,
|
|
size_t *output_count
|
|
)
|
|
{
|
|
static const char *const SQL =
|
|
"SELECT exercise_id, name, tracking_mode, "
|
|
"recording_mode, data_fields "
|
|
"FROM exercises "
|
|
"ORDER BY name COLLATE NOCASE, exercise_id;";
|
|
|
|
sqlite3_stmt *statement = NULL;
|
|
size_t count = 0U;
|
|
int rc;
|
|
|
|
if (database == NULL ||
|
|
database->connection == NULL ||
|
|
output_count == NULL ||
|
|
(output == NULL && capacity != 0U)) {
|
|
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
}
|
|
|
|
*output_count = 0U;
|
|
|
|
rc = sqlite3_prepare_v2(
|
|
database->connection,
|
|
SQL,
|
|
-1,
|
|
&statement,
|
|
NULL
|
|
);
|
|
|
|
if (rc != SQLITE_OK) {
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
while ((rc = sqlite3_step(statement)) == SQLITE_ROW) {
|
|
if (output != NULL && count < capacity) {
|
|
const unsigned char *exercise_id =
|
|
sqlite3_column_text(statement, 0);
|
|
const unsigned char *name =
|
|
sqlite3_column_text(statement, 1);
|
|
const unsigned char *tracking =
|
|
sqlite3_column_text(statement, 2);
|
|
const unsigned char *recording =
|
|
sqlite3_column_text(statement, 3);
|
|
sqlite3_int64 data_fields =
|
|
sqlite3_column_int64(statement, 4);
|
|
|
|
if (exercise_id == NULL ||
|
|
name == NULL ||
|
|
tracking == NULL ||
|
|
recording == NULL ||
|
|
data_fields < 0 ||
|
|
(uint64_t)data_fields > (uint64_t)UINT32_MAX ||
|
|
(((TrainlogExerciseDataFields)data_fields) &
|
|
~TRAINLOG_EXERCISE_DATA_KNOWN_MASK) != 0U) {
|
|
(void)sqlite3_finalize(statement);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
(void)memset(
|
|
&output[count],
|
|
0,
|
|
sizeof(output[count])
|
|
);
|
|
|
|
(void)snprintf(
|
|
output[count].exercise_id,
|
|
sizeof(output[count].exercise_id),
|
|
"%s",
|
|
(const char *)exercise_id
|
|
);
|
|
|
|
(void)snprintf(
|
|
output[count].name,
|
|
sizeof(output[count].name),
|
|
"%s",
|
|
(const char *)name
|
|
);
|
|
|
|
output[count].tracking_mode =
|
|
tracking_mode_from_sql(
|
|
(const char *)tracking
|
|
);
|
|
|
|
output[count].recording_mode =
|
|
recording_mode_from_sql(
|
|
(const char *)recording
|
|
);
|
|
|
|
output[count].data_fields =
|
|
(TrainlogExerciseDataFields)data_fields;
|
|
}
|
|
|
|
++count;
|
|
}
|
|
|
|
if (rc != SQLITE_DONE) {
|
|
(void)sqlite3_finalize(statement);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
if (sqlite3_finalize(statement) != SQLITE_OK) {
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
*output_count = count;
|
|
|
|
if (output != NULL && count > capacity) {
|
|
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
}
|
|
|
|
return TRAINLOG_STATUS_OK;
|
|
}
|
|
|
|
static TrainlogStatus lookup_exercise_row_id(
|
|
TrainlogDatabase *database,
|
|
const char *exercise_id,
|
|
sqlite3_int64 *output_row_id
|
|
)
|
|
{
|
|
sqlite3_stmt *statement = NULL;
|
|
int rc;
|
|
|
|
rc = sqlite3_prepare_v2(
|
|
database->connection,
|
|
"SELECT id FROM exercises WHERE exercise_id = ?1;",
|
|
-1,
|
|
&statement,
|
|
NULL
|
|
);
|
|
if (rc != SQLITE_OK) {
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
if (sqlite3_bind_text(
|
|
statement,
|
|
1,
|
|
exercise_id,
|
|
-1,
|
|
SQLITE_TRANSIENT
|
|
) != SQLITE_OK) {
|
|
(void)sqlite3_finalize(statement);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
rc = sqlite3_step(statement);
|
|
if (rc == SQLITE_DONE) {
|
|
(void)sqlite3_finalize(statement);
|
|
return TRAINLOG_STATUS_NOT_FOUND;
|
|
}
|
|
if (rc != SQLITE_ROW) {
|
|
(void)sqlite3_finalize(statement);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
*output_row_id = sqlite3_column_int64(statement, 0);
|
|
|
|
return sqlite3_finalize(statement) == SQLITE_OK
|
|
? TRAINLOG_STATUS_OK
|
|
: TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
static TrainlogStatus lookup_session_row_id(
|
|
TrainlogDatabase *database,
|
|
const char *session_id,
|
|
sqlite3_int64 *output_row_id
|
|
)
|
|
{
|
|
sqlite3_stmt *statement = NULL;
|
|
int rc;
|
|
|
|
if (database == NULL ||
|
|
database->connection == NULL ||
|
|
session_id == NULL ||
|
|
session_id[0] == '\0' ||
|
|
output_row_id == NULL) {
|
|
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
}
|
|
|
|
rc = sqlite3_prepare_v2(
|
|
database->connection,
|
|
"SELECT id FROM sessions WHERE session_id = ?1;",
|
|
-1,
|
|
&statement,
|
|
NULL
|
|
);
|
|
if (rc != SQLITE_OK) {
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
rc = sqlite3_bind_text(
|
|
statement,
|
|
1,
|
|
session_id,
|
|
-1,
|
|
SQLITE_TRANSIENT
|
|
);
|
|
if (rc != SQLITE_OK) {
|
|
(void)sqlite3_finalize(statement);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
rc = sqlite3_step(statement);
|
|
|
|
if (rc == SQLITE_DONE) {
|
|
(void)sqlite3_finalize(statement);
|
|
return TRAINLOG_STATUS_NOT_FOUND;
|
|
}
|
|
|
|
if (rc != SQLITE_ROW) {
|
|
(void)sqlite3_finalize(statement);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
*output_row_id =
|
|
sqlite3_column_int64(statement, 0);
|
|
|
|
return sqlite3_finalize(statement) == SQLITE_OK
|
|
? TRAINLOG_STATUS_OK
|
|
: TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
static TrainlogStatus insert_session_header(
|
|
TrainlogDatabase *database,
|
|
const TrainlogSessionInput *session,
|
|
sqlite3_int64 *output_row_id
|
|
)
|
|
{
|
|
static const char *const SQL =
|
|
"INSERT INTO sessions("
|
|
"session_id, started_at, ended_at, session_type, notes"
|
|
") VALUES(?1, ?2, ?3, ?4, ?5);";
|
|
|
|
sqlite3_stmt *statement = NULL;
|
|
const char *session_type;
|
|
int rc;
|
|
|
|
session_type =
|
|
session_type_to_sql(
|
|
session->session_type
|
|
);
|
|
|
|
if (session_type == NULL) {
|
|
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
}
|
|
|
|
rc = sqlite3_prepare_v2(
|
|
database->connection,
|
|
SQL,
|
|
-1,
|
|
&statement,
|
|
NULL
|
|
);
|
|
if (rc != SQLITE_OK) {
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
rc = sqlite3_bind_text(
|
|
statement,
|
|
1,
|
|
session->session_id,
|
|
-1,
|
|
SQLITE_TRANSIENT
|
|
);
|
|
|
|
if (rc == SQLITE_OK) {
|
|
rc = sqlite3_bind_text(
|
|
statement,
|
|
2,
|
|
session->started_at,
|
|
-1,
|
|
SQLITE_TRANSIENT
|
|
);
|
|
}
|
|
|
|
if (rc == SQLITE_OK) {
|
|
rc =
|
|
session->ended_at[0] != '\0'
|
|
? sqlite3_bind_text(
|
|
statement,
|
|
3,
|
|
session->ended_at,
|
|
-1,
|
|
SQLITE_TRANSIENT
|
|
)
|
|
: sqlite3_bind_null(
|
|
statement,
|
|
3
|
|
);
|
|
}
|
|
|
|
if (rc == SQLITE_OK) {
|
|
rc = sqlite3_bind_text(
|
|
statement,
|
|
4,
|
|
session_type,
|
|
-1,
|
|
SQLITE_STATIC
|
|
);
|
|
}
|
|
|
|
if (rc == SQLITE_OK) {
|
|
rc =
|
|
session->notes != NULL &&
|
|
session->notes[0] != '\0'
|
|
? sqlite3_bind_text(
|
|
statement,
|
|
5,
|
|
session->notes,
|
|
-1,
|
|
SQLITE_TRANSIENT
|
|
)
|
|
: sqlite3_bind_null(
|
|
statement,
|
|
5
|
|
);
|
|
}
|
|
|
|
if (rc != SQLITE_OK) {
|
|
(void)sqlite3_finalize(statement);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
rc = sqlite3_step(statement);
|
|
|
|
if (rc == SQLITE_CONSTRAINT) {
|
|
(void)sqlite3_finalize(statement);
|
|
return TRAINLOG_STATUS_CONFLICT;
|
|
}
|
|
|
|
if (rc != SQLITE_DONE) {
|
|
(void)sqlite3_finalize(statement);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
if (sqlite3_finalize(statement) != SQLITE_OK) {
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
*output_row_id =
|
|
sqlite3_last_insert_rowid(
|
|
database->connection
|
|
);
|
|
|
|
return TRAINLOG_STATUS_OK;
|
|
}
|
|
|
|
static TrainlogStatus insert_session_exercise(
|
|
TrainlogDatabase *database,
|
|
sqlite3_int64 session_row_id,
|
|
size_t position,
|
|
const TrainlogSessionExerciseInput *input,
|
|
sqlite3_int64 *output_row_id
|
|
)
|
|
{
|
|
sqlite3_stmt *statement = NULL;
|
|
sqlite3_int64 exercise_row_id;
|
|
const char *load_mode;
|
|
const char *recording_mode;
|
|
char generated_entry_id[TRAINLOG_GENERATED_ID_CAPACITY];
|
|
const char *entry_id;
|
|
int rc;
|
|
TrainlogStatus status;
|
|
|
|
if (input == NULL ||
|
|
output_row_id == NULL ||
|
|
(input->data_fields &
|
|
~TRAINLOG_EXERCISE_DATA_KNOWN_MASK) != 0U) {
|
|
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
}
|
|
|
|
if (input->equipment_id[0] != '\0' &&
|
|
trainlog_equipment_catalog_lookup(input->equipment_id) == NULL &&
|
|
!custom_equipment_exists(database, input->equipment_id)) {
|
|
/* INVARIANT: a persisted occurrence names a known canonical or custom definition. */
|
|
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
}
|
|
|
|
/* WHY: entry_id, not position or exercise_id, is the durable identity of
|
|
* one occurrence. Local TUI creation therefore allocates it exactly once
|
|
* at persistence, while imported V2 identities pass through unchanged. */
|
|
if (input->entry_id[0] == '\0') {
|
|
status = trainlog_id_generate("sxe", generated_entry_id,
|
|
sizeof(generated_entry_id));
|
|
if (status != TRAINLOG_STATUS_OK) {
|
|
return status;
|
|
}
|
|
entry_id = generated_entry_id;
|
|
} else {
|
|
entry_id = input->entry_id;
|
|
}
|
|
|
|
load_mode =
|
|
load_mode_to_sql(
|
|
input->load_mode
|
|
);
|
|
|
|
recording_mode =
|
|
recording_mode_to_sql(
|
|
input->recording_mode
|
|
);
|
|
|
|
if (load_mode == NULL ||
|
|
recording_mode == NULL) {
|
|
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
}
|
|
|
|
if (input->recording_mode ==
|
|
TRAINLOG_RECORDING_CONTINUOUS) {
|
|
bool speed_required =
|
|
(input->data_fields &
|
|
TRAINLOG_EXERCISE_DATA_SPEED_KMH) != 0U;
|
|
|
|
bool distance_required =
|
|
(input->data_fields &
|
|
TRAINLOG_EXERCISE_DATA_DISTANCE_KM) != 0U;
|
|
|
|
if (input->load_mode != TRAINLOG_LOAD_NONE ||
|
|
input->rest_seconds != 0 ||
|
|
input->target_sets != 0 ||
|
|
input->target_reps != 0 ||
|
|
input->target_duration_seconds != 0 ||
|
|
input->target_has_weight ||
|
|
input->set_count != 0U ||
|
|
input->continuous_duration_seconds <= 0 ||
|
|
input->continuous_has_speed != speed_required ||
|
|
input->continuous_has_distance != distance_required ||
|
|
(input->continuous_has_speed &&
|
|
input->continuous_speed_kmh <= 0.0) ||
|
|
(input->continuous_has_distance &&
|
|
input->continuous_distance_km <= 0.0)) {
|
|
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
}
|
|
} else if (
|
|
input->recording_mode ==
|
|
TRAINLOG_RECORDING_SETS
|
|
) {
|
|
bool has_target_sets =
|
|
input->target_sets > 0;
|
|
|
|
bool has_target_reps =
|
|
input->target_reps > 0;
|
|
|
|
bool has_target_duration =
|
|
input->target_duration_seconds > 0;
|
|
|
|
bool any_target =
|
|
has_target_sets ||
|
|
has_target_reps ||
|
|
has_target_duration;
|
|
|
|
bool target_valid =
|
|
!any_target ||
|
|
(
|
|
has_target_sets &&
|
|
(
|
|
has_target_reps !=
|
|
has_target_duration
|
|
)
|
|
);
|
|
|
|
if (
|
|
input->target_sets < 0 ||
|
|
input->target_reps < 0 ||
|
|
input->target_duration_seconds < 0 ||
|
|
!target_valid ||
|
|
input->continuous_duration_seconds != 0 ||
|
|
input->continuous_has_speed ||
|
|
input->continuous_has_distance
|
|
) {
|
|
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
}
|
|
} else {
|
|
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
}
|
|
|
|
status = lookup_exercise_row_id(
|
|
database,
|
|
input->exercise_id,
|
|
&exercise_row_id
|
|
);
|
|
|
|
if (status != TRAINLOG_STATUS_OK) {
|
|
return status;
|
|
}
|
|
|
|
rc = sqlite3_prepare_v2(
|
|
database->connection,
|
|
"INSERT INTO session_exercises("
|
|
"entry_id, session_row_id, exercise_row_id, "
|
|
"recording_mode, data_fields, "
|
|
"position, load_mode, rest_seconds, "
|
|
"target_sets, target_reps, "
|
|
"target_duration_seconds, "
|
|
"target_weight_kg, notes, equipment_id"
|
|
") VALUES("
|
|
"?1, ?2, ?3, ?4, ?5, ?6, ?7, "
|
|
"?8, ?9, ?10, ?11, ?12, ?13, ?14"
|
|
");",
|
|
-1,
|
|
&statement,
|
|
NULL
|
|
);
|
|
|
|
if (rc != SQLITE_OK) {
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
rc = sqlite3_bind_text(statement, 1, entry_id, -1, SQLITE_TRANSIENT);
|
|
|
|
if (rc == SQLITE_OK) {
|
|
rc = sqlite3_bind_int64(
|
|
statement,
|
|
2,
|
|
session_row_id
|
|
);
|
|
}
|
|
|
|
if (rc == SQLITE_OK) {
|
|
rc = sqlite3_bind_int64(
|
|
statement,
|
|
3,
|
|
exercise_row_id
|
|
);
|
|
}
|
|
|
|
if (rc == SQLITE_OK) {
|
|
rc = sqlite3_bind_text(
|
|
statement,
|
|
4,
|
|
recording_mode,
|
|
-1,
|
|
SQLITE_STATIC
|
|
);
|
|
}
|
|
|
|
if (rc == SQLITE_OK) {
|
|
rc = sqlite3_bind_int64(
|
|
statement,
|
|
5,
|
|
(sqlite3_int64)input->data_fields
|
|
);
|
|
}
|
|
|
|
if (rc == SQLITE_OK) {
|
|
rc = sqlite3_bind_int64(
|
|
statement,
|
|
6,
|
|
(sqlite3_int64)position
|
|
);
|
|
}
|
|
|
|
if (rc == SQLITE_OK) {
|
|
rc = sqlite3_bind_text(
|
|
statement,
|
|
7,
|
|
load_mode,
|
|
-1,
|
|
SQLITE_STATIC
|
|
);
|
|
}
|
|
|
|
if (rc == SQLITE_OK) {
|
|
rc = sqlite3_bind_int(
|
|
statement,
|
|
8,
|
|
input->rest_seconds
|
|
);
|
|
}
|
|
|
|
if (rc == SQLITE_OK) {
|
|
rc = input->target_sets > 0
|
|
? sqlite3_bind_int(
|
|
statement,
|
|
9,
|
|
input->target_sets
|
|
)
|
|
: sqlite3_bind_null(
|
|
statement,
|
|
9
|
|
);
|
|
}
|
|
|
|
if (rc == SQLITE_OK) {
|
|
rc = input->target_reps > 0
|
|
? sqlite3_bind_int(
|
|
statement,
|
|
10,
|
|
input->target_reps
|
|
)
|
|
: sqlite3_bind_null(
|
|
statement,
|
|
10
|
|
);
|
|
}
|
|
|
|
if (rc == SQLITE_OK) {
|
|
rc = input->target_duration_seconds > 0
|
|
? sqlite3_bind_int(
|
|
statement,
|
|
11,
|
|
input->target_duration_seconds
|
|
)
|
|
: sqlite3_bind_null(
|
|
statement,
|
|
11
|
|
);
|
|
}
|
|
|
|
if (rc == SQLITE_OK) {
|
|
rc = input->target_has_weight
|
|
? sqlite3_bind_double(
|
|
statement,
|
|
12,
|
|
input->target_weight_kg
|
|
)
|
|
: sqlite3_bind_null(
|
|
statement,
|
|
12
|
|
);
|
|
}
|
|
|
|
if (rc == SQLITE_OK) {
|
|
rc =
|
|
input->notes != NULL &&
|
|
input->notes[0] != '\0'
|
|
? sqlite3_bind_text(
|
|
statement,
|
|
13,
|
|
input->notes,
|
|
-1,
|
|
SQLITE_TRANSIENT
|
|
)
|
|
: sqlite3_bind_null(
|
|
statement,
|
|
13
|
|
);
|
|
}
|
|
|
|
if (rc == SQLITE_OK) {
|
|
rc = input->equipment_id[0] != '\0'
|
|
? sqlite3_bind_text(statement, 14, input->equipment_id, -1,
|
|
SQLITE_TRANSIENT)
|
|
: sqlite3_bind_null(statement, 14);
|
|
}
|
|
|
|
if (rc != SQLITE_OK) {
|
|
(void)sqlite3_finalize(statement);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
rc = sqlite3_step(statement);
|
|
|
|
if (rc == SQLITE_CONSTRAINT) {
|
|
(void)sqlite3_finalize(statement);
|
|
return TRAINLOG_STATUS_CONFLICT;
|
|
}
|
|
|
|
if (rc != SQLITE_DONE) {
|
|
(void)sqlite3_finalize(statement);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
if (sqlite3_finalize(statement) != SQLITE_OK) {
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
*output_row_id =
|
|
sqlite3_last_insert_rowid(
|
|
database->connection
|
|
);
|
|
|
|
return TRAINLOG_STATUS_OK;
|
|
}
|
|
|
|
static TrainlogStatus insert_continuous_activity(
|
|
TrainlogDatabase *database,
|
|
sqlite3_int64 session_exercise_row_id,
|
|
const TrainlogSessionExerciseInput *input
|
|
)
|
|
{
|
|
sqlite3_stmt *statement = NULL;
|
|
int rc;
|
|
|
|
if (database == NULL ||
|
|
database->connection == NULL ||
|
|
input == NULL ||
|
|
input->recording_mode != TRAINLOG_RECORDING_CONTINUOUS ||
|
|
input->continuous_duration_seconds <= 0) {
|
|
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
}
|
|
|
|
rc = sqlite3_prepare_v2(
|
|
database->connection,
|
|
"INSERT INTO continuous_activity("
|
|
"session_exercise_row_id, "
|
|
"duration_seconds, speed_kmh, "
|
|
"distance_km"
|
|
") VALUES(?1, ?2, ?3, ?4);",
|
|
-1,
|
|
&statement,
|
|
NULL
|
|
);
|
|
|
|
if (rc != SQLITE_OK) {
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
rc = sqlite3_bind_int64(
|
|
statement,
|
|
1,
|
|
session_exercise_row_id
|
|
);
|
|
|
|
if (rc == SQLITE_OK) {
|
|
rc = sqlite3_bind_int(
|
|
statement,
|
|
2,
|
|
input->continuous_duration_seconds
|
|
);
|
|
}
|
|
|
|
if (rc == SQLITE_OK) {
|
|
rc = input->continuous_has_speed
|
|
? sqlite3_bind_double(
|
|
statement,
|
|
3,
|
|
input->continuous_speed_kmh
|
|
)
|
|
: sqlite3_bind_null(
|
|
statement,
|
|
3
|
|
);
|
|
}
|
|
|
|
if (rc == SQLITE_OK) {
|
|
rc = input->continuous_has_distance
|
|
? sqlite3_bind_double(
|
|
statement,
|
|
4,
|
|
input->continuous_distance_km
|
|
)
|
|
: sqlite3_bind_null(
|
|
statement,
|
|
4
|
|
);
|
|
}
|
|
|
|
if (rc != SQLITE_OK) {
|
|
(void)sqlite3_finalize(statement);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
rc = sqlite3_step(statement);
|
|
|
|
if (rc == SQLITE_CONSTRAINT) {
|
|
(void)sqlite3_finalize(statement);
|
|
return TRAINLOG_STATUS_CONFLICT;
|
|
}
|
|
|
|
if (rc != SQLITE_DONE) {
|
|
(void)sqlite3_finalize(statement);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
return sqlite3_finalize(statement) == SQLITE_OK
|
|
? TRAINLOG_STATUS_OK
|
|
: TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
static TrainlogStatus insert_performed_set(
|
|
TrainlogDatabase *database,
|
|
sqlite3_int64 session_exercise_row_id,
|
|
size_t position,
|
|
const TrainlogSetInput *input
|
|
)
|
|
{
|
|
sqlite3_stmt *statement = NULL;
|
|
int rc;
|
|
|
|
rc = sqlite3_prepare_v2(
|
|
database->connection,
|
|
"INSERT INTO performed_sets("
|
|
"session_exercise_row_id, position, reps, duration_seconds, weight_kg"
|
|
") VALUES(?1, ?2, ?3, ?4, ?5);",
|
|
-1,
|
|
&statement,
|
|
NULL
|
|
);
|
|
if (rc != SQLITE_OK) {
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
rc = sqlite3_bind_int64(statement, 1, session_exercise_row_id);
|
|
if (rc == SQLITE_OK) {
|
|
rc = sqlite3_bind_int64(statement, 2, (sqlite3_int64)position);
|
|
}
|
|
if (rc == SQLITE_OK) {
|
|
rc = input->duration_seconds > 0
|
|
? sqlite3_bind_null(statement, 3)
|
|
: sqlite3_bind_int(statement, 3, input->reps);
|
|
}
|
|
if (rc == SQLITE_OK) {
|
|
rc = input->duration_seconds > 0
|
|
? sqlite3_bind_int(statement, 4, input->duration_seconds)
|
|
: sqlite3_bind_null(statement, 4);
|
|
}
|
|
if (rc == SQLITE_OK) {
|
|
rc = input->has_weight
|
|
? sqlite3_bind_double(statement, 5, input->weight_kg)
|
|
: sqlite3_bind_null(statement, 5);
|
|
}
|
|
|
|
if (rc != SQLITE_OK) {
|
|
(void)sqlite3_finalize(statement);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
rc = sqlite3_step(statement);
|
|
if (rc == SQLITE_CONSTRAINT) {
|
|
(void)sqlite3_finalize(statement);
|
|
return TRAINLOG_STATUS_CONFLICT;
|
|
}
|
|
if (rc != SQLITE_DONE) {
|
|
(void)sqlite3_finalize(statement);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
return sqlite3_finalize(statement) == SQLITE_OK
|
|
? TRAINLOG_STATUS_OK
|
|
: TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
static TrainlogStatus insert_session_children(
|
|
TrainlogDatabase *database,
|
|
sqlite3_int64 session_row_id,
|
|
const TrainlogSessionExerciseInput *exercises,
|
|
size_t exercise_count
|
|
)
|
|
{
|
|
size_t exercise_index;
|
|
|
|
if (database == NULL ||
|
|
database->connection == NULL ||
|
|
(exercise_count > 0U &&
|
|
exercises == NULL)) {
|
|
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
}
|
|
|
|
for (exercise_index = 0U;
|
|
exercise_index < exercise_count;
|
|
++exercise_index) {
|
|
const TrainlogSessionExerciseInput *exercise =
|
|
&exercises[exercise_index];
|
|
|
|
sqlite3_int64 session_exercise_row_id;
|
|
size_t set_index;
|
|
TrainlogStatus status;
|
|
|
|
status = insert_session_exercise(
|
|
database,
|
|
session_row_id,
|
|
exercise_index,
|
|
exercise,
|
|
&session_exercise_row_id
|
|
);
|
|
|
|
if (status != TRAINLOG_STATUS_OK) {
|
|
return status;
|
|
}
|
|
|
|
if (exercise->recording_mode ==
|
|
TRAINLOG_RECORDING_CONTINUOUS) {
|
|
status = insert_continuous_activity(
|
|
database,
|
|
session_exercise_row_id,
|
|
exercise
|
|
);
|
|
|
|
if (status != TRAINLOG_STATUS_OK) {
|
|
return status;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
for (set_index = 0U;
|
|
set_index < exercise->set_count;
|
|
++set_index) {
|
|
if (exercise->sets == NULL) {
|
|
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
}
|
|
|
|
status = insert_performed_set(
|
|
database,
|
|
session_exercise_row_id,
|
|
set_index,
|
|
&exercise->sets[set_index]
|
|
);
|
|
|
|
if (status != TRAINLOG_STATUS_OK) {
|
|
return status;
|
|
}
|
|
}
|
|
}
|
|
|
|
return TRAINLOG_STATUS_OK;
|
|
}
|
|
|
|
TrainlogStatus trainlog_database_insert_session(
|
|
TrainlogDatabase *database,
|
|
const TrainlogSessionInput *session
|
|
)
|
|
{
|
|
sqlite3_int64 session_row_id;
|
|
TrainlogStatus status;
|
|
|
|
if (database == NULL ||
|
|
database->connection == NULL ||
|
|
session == NULL ||
|
|
session->session_id[0] == '\0' ||
|
|
session->started_at[0] == '\0' ||
|
|
(session->exercise_count > 0U &&
|
|
session->exercises == NULL)) {
|
|
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
}
|
|
|
|
status = trainlog_database_begin(database);
|
|
if (status != TRAINLOG_STATUS_OK) {
|
|
return status;
|
|
}
|
|
|
|
status = insert_session_header(
|
|
database,
|
|
session,
|
|
&session_row_id
|
|
);
|
|
|
|
if (status == TRAINLOG_STATUS_OK) {
|
|
status = insert_session_children(
|
|
database,
|
|
session_row_id,
|
|
session->exercises,
|
|
session->exercise_count
|
|
);
|
|
}
|
|
|
|
if (status != TRAINLOG_STATUS_OK) {
|
|
(void)trainlog_database_rollback(database);
|
|
return status;
|
|
}
|
|
|
|
status = trainlog_database_commit(database);
|
|
|
|
if (status != TRAINLOG_STATUS_OK) {
|
|
(void)trainlog_database_rollback(database);
|
|
}
|
|
|
|
return status;
|
|
}
|
|
|
|
TrainlogStatus trainlog_database_replace_session_exercises(
|
|
TrainlogDatabase *database,
|
|
const char *session_id,
|
|
const TrainlogSessionExerciseInput *exercises,
|
|
size_t exercise_count
|
|
)
|
|
{
|
|
sqlite3_int64 session_row_id;
|
|
sqlite3_stmt *statement = NULL;
|
|
TrainlogStatus status;
|
|
int rc;
|
|
|
|
if (database == NULL ||
|
|
database->connection == NULL ||
|
|
session_id == NULL ||
|
|
session_id[0] == '\0' ||
|
|
(exercise_count > 0U && exercises == NULL)) {
|
|
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
}
|
|
|
|
status = trainlog_database_begin(database);
|
|
if (status != TRAINLOG_STATUS_OK) {
|
|
return status;
|
|
}
|
|
|
|
status = lookup_session_row_id(
|
|
database,
|
|
session_id,
|
|
&session_row_id
|
|
);
|
|
|
|
if (status != TRAINLOG_STATUS_OK) {
|
|
(void)trainlog_database_rollback(database);
|
|
return status;
|
|
}
|
|
|
|
rc = sqlite3_prepare_v2(
|
|
database->connection,
|
|
"DELETE FROM session_exercises "
|
|
"WHERE session_row_id = ?1;",
|
|
-1,
|
|
&statement,
|
|
NULL
|
|
);
|
|
|
|
if (rc == SQLITE_OK) {
|
|
rc = sqlite3_bind_int64(
|
|
statement,
|
|
1,
|
|
session_row_id
|
|
);
|
|
}
|
|
|
|
if (rc == SQLITE_OK) {
|
|
rc = sqlite3_step(statement);
|
|
}
|
|
|
|
if (rc != SQLITE_DONE) {
|
|
(void)sqlite3_finalize(statement);
|
|
(void)trainlog_database_rollback(database);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
if (sqlite3_finalize(statement) != SQLITE_OK) {
|
|
(void)trainlog_database_rollback(database);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
statement = NULL;
|
|
|
|
status = insert_session_children(
|
|
database,
|
|
session_row_id,
|
|
exercises,
|
|
exercise_count
|
|
);
|
|
|
|
if (status != TRAINLOG_STATUS_OK) {
|
|
(void)trainlog_database_rollback(database);
|
|
return status;
|
|
}
|
|
|
|
status = trainlog_database_commit(database);
|
|
|
|
if (status != TRAINLOG_STATUS_OK) {
|
|
(void)trainlog_database_rollback(database);
|
|
}
|
|
|
|
return status;
|
|
}
|
|
|
|
TrainlogStatus trainlog_database_list_sessions(
|
|
TrainlogDatabase *database,
|
|
TrainlogSessionSummary *output,
|
|
size_t capacity,
|
|
size_t *output_count
|
|
)
|
|
{
|
|
static const char *const SQL =
|
|
"SELECT "
|
|
"s.session_id, "
|
|
"s.started_at, "
|
|
"COALESCE(s.ended_at, ''), "
|
|
"s.session_type, "
|
|
"COUNT(se.id) "
|
|
"FROM sessions AS s "
|
|
"LEFT JOIN session_exercises AS se "
|
|
"ON se.session_row_id = s.id "
|
|
"GROUP BY s.id "
|
|
"ORDER BY s.started_at DESC, s.id DESC;";
|
|
|
|
sqlite3_stmt *statement = NULL;
|
|
size_t count = 0U;
|
|
int rc;
|
|
|
|
if (database == NULL ||
|
|
database->connection == NULL ||
|
|
output_count == NULL ||
|
|
(capacity > 0U && output == NULL)) {
|
|
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
}
|
|
|
|
rc = sqlite3_prepare_v2(
|
|
database->connection,
|
|
SQL,
|
|
-1,
|
|
&statement,
|
|
NULL
|
|
);
|
|
if (rc != SQLITE_OK) {
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
while ((rc = sqlite3_step(statement)) == SQLITE_ROW) {
|
|
if (count < capacity) {
|
|
const unsigned char *id =
|
|
sqlite3_column_text(statement, 0);
|
|
|
|
const unsigned char *started =
|
|
sqlite3_column_text(statement, 1);
|
|
|
|
const unsigned char *ended =
|
|
sqlite3_column_text(statement, 2);
|
|
|
|
const unsigned char *session_type =
|
|
sqlite3_column_text(statement, 3);
|
|
|
|
sqlite3_int64 exercise_count =
|
|
sqlite3_column_int64(statement, 4);
|
|
|
|
if (id == NULL ||
|
|
started == NULL ||
|
|
ended == NULL ||
|
|
session_type == NULL ||
|
|
exercise_count < 0) {
|
|
(void)sqlite3_finalize(statement);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
(void)snprintf(
|
|
output[count].session_id,
|
|
sizeof(output[count].session_id),
|
|
"%s",
|
|
(const char *)id
|
|
);
|
|
|
|
(void)snprintf(
|
|
output[count].started_at,
|
|
sizeof(output[count].started_at),
|
|
"%s",
|
|
(const char *)started
|
|
);
|
|
|
|
(void)snprintf(
|
|
output[count].ended_at,
|
|
sizeof(output[count].ended_at),
|
|
"%s",
|
|
(const char *)ended
|
|
);
|
|
|
|
if (!session_type_from_sql(
|
|
(const char *)session_type,
|
|
&output[count].session_type
|
|
)) {
|
|
(void)sqlite3_finalize(statement);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
output[count].exercise_count =
|
|
(size_t)exercise_count;
|
|
}
|
|
|
|
++count;
|
|
}
|
|
|
|
if (rc != SQLITE_DONE) {
|
|
(void)sqlite3_finalize(statement);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
if (sqlite3_finalize(statement) != SQLITE_OK) {
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
*output_count =
|
|
count < capacity
|
|
? count
|
|
: capacity;
|
|
|
|
return TRAINLOG_STATUS_OK;
|
|
}
|
|
|
|
static int bind_optional_double(
|
|
sqlite3_stmt *statement,
|
|
int index,
|
|
bool present,
|
|
double value
|
|
)
|
|
{
|
|
return present
|
|
? sqlite3_bind_double(statement, index, value)
|
|
: sqlite3_bind_null(statement, index);
|
|
}
|
|
|
|
TrainlogStatus trainlog_database_insert_body_observation(
|
|
TrainlogDatabase *database,
|
|
const TrainlogBodyObservationInput *observation
|
|
)
|
|
{
|
|
static const char *const SQL =
|
|
"INSERT INTO body_observations("
|
|
"observation_id, observed_at, session_row_id, 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"
|
|
") VALUES("
|
|
"?1, ?2, "
|
|
"(SELECT id FROM sessions WHERE session_id = ?3), "
|
|
"?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18"
|
|
");";
|
|
sqlite3_stmt *statement = NULL;
|
|
bool any_metric;
|
|
int rc;
|
|
|
|
if (database == NULL ||
|
|
database->connection == NULL ||
|
|
observation == NULL ||
|
|
observation->observation_id[0] == '\0' ||
|
|
observation->observed_at[0] == '\0') {
|
|
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
}
|
|
|
|
any_metric =
|
|
observation->has_body_weight ||
|
|
observation->has_neck ||
|
|
observation->has_shoulders ||
|
|
observation->has_chest ||
|
|
observation->has_waist ||
|
|
observation->has_hips ||
|
|
observation->has_left_arm ||
|
|
observation->has_right_arm ||
|
|
observation->has_left_forearm ||
|
|
observation->has_right_forearm ||
|
|
observation->has_left_thigh ||
|
|
observation->has_right_thigh ||
|
|
observation->has_left_calf ||
|
|
observation->has_right_calf;
|
|
|
|
if (!any_metric) {
|
|
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
}
|
|
|
|
rc = sqlite3_prepare_v2(database->connection, SQL, -1, &statement, NULL);
|
|
if (rc != SQLITE_OK) {
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
rc = sqlite3_bind_text(
|
|
statement,
|
|
1,
|
|
observation->observation_id,
|
|
-1,
|
|
SQLITE_TRANSIENT
|
|
);
|
|
if (rc == SQLITE_OK) {
|
|
rc = sqlite3_bind_text(
|
|
statement,
|
|
2,
|
|
observation->observed_at,
|
|
-1,
|
|
SQLITE_TRANSIENT
|
|
);
|
|
}
|
|
|
|
if (rc == SQLITE_OK) {
|
|
rc = observation->session_id != NULL &&
|
|
observation->session_id[0] != '\0'
|
|
? sqlite3_bind_text(
|
|
statement,
|
|
3,
|
|
observation->session_id,
|
|
-1,
|
|
SQLITE_TRANSIENT
|
|
)
|
|
: sqlite3_bind_null(statement, 3);
|
|
}
|
|
|
|
#define BIND_METRIC(index_, flag_, value_) \
|
|
do { \
|
|
if (rc == SQLITE_OK) { \
|
|
rc = bind_optional_double( \
|
|
statement, \
|
|
(index_), \
|
|
(flag_), \
|
|
(value_) \
|
|
); \
|
|
} \
|
|
} while (0)
|
|
|
|
BIND_METRIC(4, observation->has_body_weight, observation->body_weight_kg);
|
|
BIND_METRIC(5, observation->has_neck, observation->neck_cm);
|
|
BIND_METRIC(6, observation->has_shoulders, observation->shoulders_cm);
|
|
BIND_METRIC(7, observation->has_chest, observation->chest_cm);
|
|
BIND_METRIC(8, observation->has_waist, observation->waist_cm);
|
|
BIND_METRIC(9, observation->has_hips, observation->hips_cm);
|
|
BIND_METRIC(10, observation->has_left_arm, observation->left_arm_cm);
|
|
BIND_METRIC(11, observation->has_right_arm, observation->right_arm_cm);
|
|
BIND_METRIC(
|
|
12,
|
|
observation->has_left_forearm,
|
|
observation->left_forearm_cm
|
|
);
|
|
BIND_METRIC(
|
|
13,
|
|
observation->has_right_forearm,
|
|
observation->right_forearm_cm
|
|
);
|
|
BIND_METRIC(14, observation->has_left_thigh, observation->left_thigh_cm);
|
|
BIND_METRIC(15, observation->has_right_thigh, observation->right_thigh_cm);
|
|
BIND_METRIC(16, observation->has_left_calf, observation->left_calf_cm);
|
|
BIND_METRIC(17, observation->has_right_calf, observation->right_calf_cm);
|
|
|
|
#undef BIND_METRIC
|
|
|
|
if (rc == SQLITE_OK) {
|
|
rc = observation->notes != NULL && observation->notes[0] != '\0'
|
|
? sqlite3_bind_text(
|
|
statement,
|
|
18,
|
|
observation->notes,
|
|
-1,
|
|
SQLITE_TRANSIENT
|
|
)
|
|
: sqlite3_bind_null(statement, 18);
|
|
}
|
|
|
|
if (rc != SQLITE_OK) {
|
|
(void)sqlite3_finalize(statement);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
rc = sqlite3_step(statement);
|
|
if (rc == SQLITE_CONSTRAINT) {
|
|
(void)sqlite3_finalize(statement);
|
|
return TRAINLOG_STATUS_CONFLICT;
|
|
}
|
|
if (rc != SQLITE_DONE) {
|
|
(void)sqlite3_finalize(statement);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
return sqlite3_finalize(statement) == SQLITE_OK
|
|
? TRAINLOG_STATUS_OK
|
|
: TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
TrainlogStatus trainlog_database_list_weight_points(
|
|
TrainlogDatabase *database,
|
|
TrainlogWeightPoint *output,
|
|
size_t capacity,
|
|
size_t *output_count
|
|
)
|
|
{
|
|
sqlite3_stmt *statement = NULL;
|
|
size_t count = 0U;
|
|
int rc;
|
|
|
|
if (database == NULL ||
|
|
database->connection == NULL ||
|
|
output_count == NULL ||
|
|
(capacity > 0U && output == NULL)) {
|
|
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
}
|
|
|
|
rc = sqlite3_prepare_v2(
|
|
database->connection,
|
|
"SELECT observed_at, body_weight_kg "
|
|
"FROM body_observations "
|
|
"WHERE body_weight_kg IS NOT NULL "
|
|
"ORDER BY observed_at ASC, id ASC;",
|
|
-1,
|
|
&statement,
|
|
NULL
|
|
);
|
|
if (rc != SQLITE_OK) {
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
while ((rc = sqlite3_step(statement)) == SQLITE_ROW) {
|
|
if (count < capacity) {
|
|
const unsigned char *observed = sqlite3_column_text(statement, 0);
|
|
if (observed == NULL) {
|
|
(void)sqlite3_finalize(statement);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
(void)snprintf(
|
|
output[count].observed_at,
|
|
sizeof(output[count].observed_at),
|
|
"%s",
|
|
(const char *)observed
|
|
);
|
|
output[count].body_weight_kg =
|
|
sqlite3_column_double(statement, 1);
|
|
}
|
|
++count;
|
|
}
|
|
|
|
if (rc != SQLITE_DONE) {
|
|
(void)sqlite3_finalize(statement);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
if (sqlite3_finalize(statement) != SQLITE_OK) {
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
*output_count = count < capacity ? count : capacity;
|
|
return TRAINLOG_STATUS_OK;
|
|
}
|
|
|
|
/* TRAINLOG_SESSION_DETAILS_IMPLEMENTATION */
|
|
|
|
static TrainlogLoadMode detail_load_mode_from_text(const char *text)
|
|
{
|
|
if (text != NULL && strcmp(text, "external") == 0) {
|
|
return TRAINLOG_LOAD_EXTERNAL;
|
|
}
|
|
|
|
if (text != NULL && strcmp(text, "assistance") == 0) {
|
|
return TRAINLOG_LOAD_ASSISTANCE;
|
|
}
|
|
|
|
return TRAINLOG_LOAD_NONE;
|
|
}
|
|
|
|
static TrainlogStatus detail_append_text(
|
|
char *output,
|
|
size_t output_size,
|
|
size_t *used,
|
|
const char *text
|
|
)
|
|
{
|
|
size_t remaining;
|
|
int written;
|
|
|
|
if (output == NULL ||
|
|
used == NULL ||
|
|
text == NULL ||
|
|
*used >= output_size) {
|
|
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
}
|
|
|
|
remaining = output_size - *used;
|
|
|
|
written = snprintf(
|
|
output + *used,
|
|
remaining,
|
|
"%s",
|
|
text
|
|
);
|
|
|
|
if (written < 0) {
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
if ((size_t)written >= remaining) {
|
|
if (output_size >= 4U) {
|
|
output[output_size - 4U] = '.';
|
|
output[output_size - 3U] = '.';
|
|
output[output_size - 2U] = '.';
|
|
output[output_size - 1U] = '\0';
|
|
}
|
|
|
|
*used = output_size - 1U;
|
|
return TRAINLOG_STATUS_OK;
|
|
}
|
|
|
|
*used += (size_t)written;
|
|
return TRAINLOG_STATUS_OK;
|
|
}
|
|
|
|
static TrainlogStatus detail_fill_sets(
|
|
TrainlogDatabase *database,
|
|
sqlite3_int64 session_exercise_row_id,
|
|
TrainlogPersistedExerciseDetail *detail
|
|
)
|
|
{
|
|
static const char *const SQL =
|
|
"SELECT reps, duration_seconds, weight_kg "
|
|
"FROM performed_sets "
|
|
"WHERE session_exercise_row_id = ?1 "
|
|
"ORDER BY position ASC;";
|
|
|
|
sqlite3_stmt *statement = NULL;
|
|
size_t used = 0U;
|
|
size_t count = 0U;
|
|
int rc;
|
|
|
|
rc = sqlite3_prepare_v2(
|
|
database->connection,
|
|
SQL,
|
|
-1,
|
|
&statement,
|
|
NULL
|
|
);
|
|
if (rc != SQLITE_OK) {
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
rc = sqlite3_bind_int64(
|
|
statement,
|
|
1,
|
|
session_exercise_row_id
|
|
);
|
|
if (rc != SQLITE_OK) {
|
|
(void)sqlite3_finalize(statement);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
detail->actual_summary[0] = '\0';
|
|
|
|
while ((rc = sqlite3_step(statement)) == SQLITE_ROW) {
|
|
char fragment[96];
|
|
int written;
|
|
int has_reps =
|
|
sqlite3_column_type(statement, 0) != SQLITE_NULL;
|
|
int has_duration =
|
|
sqlite3_column_type(statement, 1) != SQLITE_NULL;
|
|
int has_weight =
|
|
sqlite3_column_type(statement, 2) != SQLITE_NULL;
|
|
|
|
if (count > 0U) {
|
|
TrainlogStatus status = detail_append_text(
|
|
detail->actual_summary,
|
|
sizeof(detail->actual_summary),
|
|
&used,
|
|
" / "
|
|
);
|
|
|
|
if (status != TRAINLOG_STATUS_OK) {
|
|
(void)sqlite3_finalize(statement);
|
|
return status;
|
|
}
|
|
}
|
|
|
|
if (has_reps != 0) {
|
|
int reps = sqlite3_column_int(statement, 0);
|
|
|
|
if (has_weight != 0) {
|
|
written = snprintf(
|
|
fragment,
|
|
sizeof(fragment),
|
|
"%d@%.1f",
|
|
reps,
|
|
sqlite3_column_double(statement, 2)
|
|
);
|
|
} else {
|
|
written = snprintf(
|
|
fragment,
|
|
sizeof(fragment),
|
|
"%d",
|
|
reps
|
|
);
|
|
}
|
|
} else if (has_duration != 0) {
|
|
int duration = sqlite3_column_int(statement, 1);
|
|
|
|
char duration_text[64];
|
|
|
|
if (trainlog_duration_format(
|
|
duration,
|
|
duration_text,
|
|
sizeof(duration_text)
|
|
) != TRAINLOG_STATUS_OK) {
|
|
(void)sqlite3_finalize(statement);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
if (has_weight != 0) {
|
|
written = snprintf(
|
|
fragment,
|
|
sizeof(fragment),
|
|
"%s@%.1f",
|
|
duration_text,
|
|
sqlite3_column_double(statement, 2)
|
|
);
|
|
} else {
|
|
written = snprintf(
|
|
fragment,
|
|
sizeof(fragment),
|
|
"%s",
|
|
duration_text
|
|
);
|
|
}
|
|
} else {
|
|
(void)sqlite3_finalize(statement);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
if (written < 0 ||
|
|
(size_t)written >= sizeof(fragment)) {
|
|
(void)sqlite3_finalize(statement);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
{
|
|
TrainlogStatus status = detail_append_text(
|
|
detail->actual_summary,
|
|
sizeof(detail->actual_summary),
|
|
&used,
|
|
fragment
|
|
);
|
|
|
|
if (status != TRAINLOG_STATUS_OK) {
|
|
(void)sqlite3_finalize(statement);
|
|
return status;
|
|
}
|
|
}
|
|
|
|
++count;
|
|
}
|
|
|
|
if (rc != SQLITE_DONE) {
|
|
(void)sqlite3_finalize(statement);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
if (sqlite3_finalize(statement) != SQLITE_OK) {
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
detail->actual_set_count = count;
|
|
|
|
if (count == 0U) {
|
|
(void)snprintf(
|
|
detail->actual_summary,
|
|
sizeof(detail->actual_summary),
|
|
"%s",
|
|
"aucune série réalisée"
|
|
);
|
|
}
|
|
|
|
return TRAINLOG_STATUS_OK;
|
|
}
|
|
|
|
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
|
|
)
|
|
{
|
|
static const char *const HEADER_SQL =
|
|
"SELECT started_at, COALESCE(ended_at, ''), session_type "
|
|
"FROM sessions "
|
|
"WHERE session_id = ?1;";
|
|
|
|
static const char *const EXERCISE_SQL =
|
|
"SELECT "
|
|
"e.name, e.tracking_mode, "
|
|
"se.recording_mode, se.data_fields, "
|
|
"se.load_mode, se.rest_seconds, "
|
|
"COALESCE(se.target_sets, 0), "
|
|
"COALESCE(se.target_reps, 0), "
|
|
"COALESCE(se.target_duration_seconds, 0), "
|
|
"se.target_weight_kg, "
|
|
"ca.duration_seconds, ca.speed_kmh, ca.distance_km, "
|
|
"se.equipment_id, "
|
|
"se.entry_id, "
|
|
"se.id "
|
|
"FROM session_exercises AS se "
|
|
"JOIN sessions AS s "
|
|
" ON s.id = se.session_row_id "
|
|
"JOIN exercises AS e "
|
|
" ON e.id = se.exercise_row_id "
|
|
"LEFT JOIN continuous_activity AS ca "
|
|
" ON ca.session_exercise_row_id = se.id "
|
|
"WHERE s.session_id = ?1 "
|
|
"ORDER BY se.position ASC;";
|
|
|
|
sqlite3_stmt *header = NULL;
|
|
sqlite3_stmt *exercises = NULL;
|
|
size_t copied = 0U;
|
|
size_t total = 0U;
|
|
int rc;
|
|
|
|
if (database == NULL ||
|
|
database->connection == NULL ||
|
|
session_id == NULL ||
|
|
session_id[0] == '\0' ||
|
|
output_session == NULL ||
|
|
output_exercise_count == NULL ||
|
|
(exercise_capacity > 0U &&
|
|
output_exercises == NULL)) {
|
|
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
}
|
|
|
|
(void)memset(
|
|
output_session,
|
|
0,
|
|
sizeof(*output_session)
|
|
);
|
|
|
|
*output_exercise_count = 0U;
|
|
|
|
rc = sqlite3_prepare_v2(
|
|
database->connection,
|
|
HEADER_SQL,
|
|
-1,
|
|
&header,
|
|
NULL
|
|
);
|
|
|
|
if (rc != SQLITE_OK) {
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
rc = sqlite3_bind_text(
|
|
header,
|
|
1,
|
|
session_id,
|
|
-1,
|
|
SQLITE_TRANSIENT
|
|
);
|
|
|
|
if (rc != SQLITE_OK) {
|
|
(void)sqlite3_finalize(header);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
rc = sqlite3_step(header);
|
|
|
|
if (rc == SQLITE_DONE) {
|
|
(void)sqlite3_finalize(header);
|
|
return TRAINLOG_STATUS_NOT_FOUND;
|
|
}
|
|
|
|
if (rc != SQLITE_ROW) {
|
|
(void)sqlite3_finalize(header);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
{
|
|
const unsigned char *started =
|
|
sqlite3_column_text(header, 0);
|
|
|
|
const unsigned char *ended =
|
|
sqlite3_column_text(header, 1);
|
|
|
|
const unsigned char *session_type =
|
|
sqlite3_column_text(header, 2);
|
|
|
|
if (started == NULL ||
|
|
ended == NULL ||
|
|
session_type == NULL) {
|
|
(void)sqlite3_finalize(header);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
(void)snprintf(
|
|
output_session->session_id,
|
|
sizeof(output_session->session_id),
|
|
"%s",
|
|
session_id
|
|
);
|
|
|
|
(void)snprintf(
|
|
output_session->started_at,
|
|
sizeof(output_session->started_at),
|
|
"%s",
|
|
(const char *)started
|
|
);
|
|
|
|
(void)snprintf(
|
|
output_session->ended_at,
|
|
sizeof(output_session->ended_at),
|
|
"%s",
|
|
(const char *)ended
|
|
);
|
|
|
|
if (!session_type_from_sql(
|
|
(const char *)session_type,
|
|
&output_session->session_type
|
|
)) {
|
|
(void)sqlite3_finalize(header);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
}
|
|
|
|
if (sqlite3_finalize(header) != SQLITE_OK) {
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
rc = sqlite3_prepare_v2(
|
|
database->connection,
|
|
EXERCISE_SQL,
|
|
-1,
|
|
&exercises,
|
|
NULL
|
|
);
|
|
|
|
if (rc != SQLITE_OK) {
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
rc = sqlite3_bind_text(
|
|
exercises,
|
|
1,
|
|
session_id,
|
|
-1,
|
|
SQLITE_TRANSIENT
|
|
);
|
|
|
|
if (rc != SQLITE_OK) {
|
|
(void)sqlite3_finalize(exercises);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
while ((rc = sqlite3_step(exercises)) == SQLITE_ROW) {
|
|
if (copied < exercise_capacity) {
|
|
TrainlogPersistedExerciseDetail *detail =
|
|
&output_exercises[copied];
|
|
|
|
const unsigned char *name =
|
|
sqlite3_column_text(exercises, 0);
|
|
|
|
const unsigned char *tracking =
|
|
sqlite3_column_text(exercises, 1);
|
|
|
|
const unsigned char *recording =
|
|
sqlite3_column_text(exercises, 2);
|
|
|
|
sqlite3_int64 data_fields =
|
|
sqlite3_column_int64(exercises, 3);
|
|
|
|
const unsigned char *load =
|
|
sqlite3_column_text(exercises, 4);
|
|
|
|
sqlite3_int64 session_exercise_row_id =
|
|
sqlite3_column_int64(exercises, 15);
|
|
const unsigned char *equipment_id =
|
|
sqlite3_column_text(exercises, 13);
|
|
const unsigned char *entry_id = sqlite3_column_text(exercises, 14);
|
|
|
|
TrainlogStatus status;
|
|
|
|
if (name == NULL ||
|
|
tracking == NULL ||
|
|
recording == NULL ||
|
|
load == NULL ||
|
|
data_fields < 0 ||
|
|
(uint64_t)data_fields >
|
|
(uint64_t)UINT32_MAX ||
|
|
(((TrainlogExerciseDataFields)data_fields) &
|
|
~TRAINLOG_EXERCISE_DATA_KNOWN_MASK) != 0U) {
|
|
(void)sqlite3_finalize(exercises);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
(void)memset(
|
|
detail,
|
|
0,
|
|
sizeof(*detail)
|
|
);
|
|
|
|
if (equipment_id != NULL) {
|
|
(void)snprintf(detail->equipment_id,
|
|
sizeof(detail->equipment_id), "%s",
|
|
(const char *)equipment_id);
|
|
}
|
|
if (entry_id == NULL) {
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
(void)snprintf(detail->entry_id, sizeof(detail->entry_id), "%s",
|
|
(const char *)entry_id);
|
|
|
|
(void)snprintf(
|
|
detail->name,
|
|
sizeof(detail->name),
|
|
"%s",
|
|
(const char *)name
|
|
);
|
|
|
|
detail->tracking_mode =
|
|
strcmp(
|
|
(const char *)tracking,
|
|
"duration"
|
|
) == 0
|
|
? TRAINLOG_TRACKING_DURATION
|
|
: TRAINLOG_TRACKING_REPS;
|
|
|
|
detail->recording_mode =
|
|
recording_mode_from_sql(
|
|
(const char *)recording
|
|
);
|
|
|
|
detail->data_fields =
|
|
(TrainlogExerciseDataFields)
|
|
data_fields;
|
|
|
|
detail->load_mode =
|
|
detail_load_mode_from_text(
|
|
(const char *)load
|
|
);
|
|
|
|
detail->rest_seconds =
|
|
sqlite3_column_int(
|
|
exercises,
|
|
5
|
|
);
|
|
|
|
detail->target_sets =
|
|
sqlite3_column_int(
|
|
exercises,
|
|
6
|
|
);
|
|
|
|
detail->target_reps =
|
|
sqlite3_column_int(
|
|
exercises,
|
|
7
|
|
);
|
|
|
|
detail->target_duration_seconds =
|
|
sqlite3_column_int(
|
|
exercises,
|
|
8
|
|
);
|
|
|
|
detail->has_target_weight =
|
|
sqlite3_column_type(
|
|
exercises,
|
|
9
|
|
) != SQLITE_NULL;
|
|
|
|
detail->target_weight_kg =
|
|
detail->has_target_weight != 0
|
|
? sqlite3_column_double(
|
|
exercises,
|
|
9
|
|
)
|
|
: 0.0;
|
|
|
|
if (detail->recording_mode ==
|
|
TRAINLOG_RECORDING_CONTINUOUS) {
|
|
bool speed_required =
|
|
(detail->data_fields &
|
|
TRAINLOG_EXERCISE_DATA_SPEED_KMH) != 0U;
|
|
|
|
bool distance_required =
|
|
(detail->data_fields &
|
|
TRAINLOG_EXERCISE_DATA_DISTANCE_KM) != 0U;
|
|
|
|
if (sqlite3_column_type(
|
|
exercises,
|
|
10
|
|
) == SQLITE_NULL) {
|
|
(void)sqlite3_finalize(exercises);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
detail->continuous_duration_seconds =
|
|
sqlite3_column_int(
|
|
exercises,
|
|
10
|
|
);
|
|
|
|
detail->has_continuous_speed =
|
|
sqlite3_column_type(
|
|
exercises,
|
|
11
|
|
) != SQLITE_NULL;
|
|
|
|
detail->continuous_speed_kmh =
|
|
detail->has_continuous_speed != 0
|
|
? sqlite3_column_double(
|
|
exercises,
|
|
11
|
|
)
|
|
: 0.0;
|
|
|
|
detail->has_continuous_distance =
|
|
sqlite3_column_type(
|
|
exercises,
|
|
12
|
|
) != SQLITE_NULL;
|
|
|
|
detail->continuous_distance_km =
|
|
detail->has_continuous_distance != 0
|
|
? sqlite3_column_double(
|
|
exercises,
|
|
12
|
|
)
|
|
: 0.0;
|
|
|
|
if ((detail->has_continuous_speed != 0) !=
|
|
speed_required ||
|
|
(detail->has_continuous_distance != 0) !=
|
|
distance_required) {
|
|
(void)sqlite3_finalize(exercises);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
detail->actual_set_count = 0U;
|
|
|
|
(void)snprintf(
|
|
detail->actual_summary,
|
|
sizeof(detail->actual_summary),
|
|
"%s",
|
|
"activité continue"
|
|
);
|
|
} else {
|
|
status = detail_fill_sets(
|
|
database,
|
|
session_exercise_row_id,
|
|
detail
|
|
);
|
|
|
|
if (status != TRAINLOG_STATUS_OK) {
|
|
(void)sqlite3_finalize(exercises);
|
|
return status;
|
|
}
|
|
}
|
|
|
|
++copied;
|
|
}
|
|
|
|
++total;
|
|
}
|
|
|
|
if (rc != SQLITE_DONE) {
|
|
(void)sqlite3_finalize(exercises);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
if (sqlite3_finalize(exercises) != SQLITE_OK) {
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
output_session->exercise_count = total;
|
|
*output_exercise_count = copied;
|
|
|
|
return TRAINLOG_STATUS_OK;
|
|
}
|
|
|
|
/* TRAINLOG_BODY_METRIC_HISTORY_IMPLEMENTATION */
|
|
|
|
static const char *body_metric_column(TrainlogBodyMetric metric)
|
|
{
|
|
switch (metric) {
|
|
case TRAINLOG_BODY_METRIC_WEIGHT:
|
|
return "body_weight_kg";
|
|
case TRAINLOG_BODY_METRIC_NECK:
|
|
return "neck_cm";
|
|
case TRAINLOG_BODY_METRIC_SHOULDERS:
|
|
return "shoulders_cm";
|
|
case TRAINLOG_BODY_METRIC_CHEST:
|
|
return "chest_cm";
|
|
case TRAINLOG_BODY_METRIC_WAIST:
|
|
return "waist_cm";
|
|
case TRAINLOG_BODY_METRIC_HIPS:
|
|
return "hips_cm";
|
|
case TRAINLOG_BODY_METRIC_LEFT_ARM:
|
|
return "left_arm_cm";
|
|
case TRAINLOG_BODY_METRIC_RIGHT_ARM:
|
|
return "right_arm_cm";
|
|
case TRAINLOG_BODY_METRIC_LEFT_FOREARM:
|
|
return "left_forearm_cm";
|
|
case TRAINLOG_BODY_METRIC_RIGHT_FOREARM:
|
|
return "right_forearm_cm";
|
|
case TRAINLOG_BODY_METRIC_LEFT_THIGH:
|
|
return "left_thigh_cm";
|
|
case TRAINLOG_BODY_METRIC_RIGHT_THIGH:
|
|
return "right_thigh_cm";
|
|
case TRAINLOG_BODY_METRIC_LEFT_CALF:
|
|
return "left_calf_cm";
|
|
case TRAINLOG_BODY_METRIC_RIGHT_CALF:
|
|
return "right_calf_cm";
|
|
case TRAINLOG_BODY_METRIC_COUNT:
|
|
default:
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
TrainlogStatus trainlog_database_list_body_metric_points(
|
|
TrainlogDatabase *database,
|
|
TrainlogBodyMetric metric,
|
|
TrainlogBodyMetricPoint *output,
|
|
size_t capacity,
|
|
size_t *output_count
|
|
)
|
|
{
|
|
const char *column;
|
|
char sql[256];
|
|
sqlite3_stmt *statement = NULL;
|
|
size_t count = 0U;
|
|
int written;
|
|
int rc;
|
|
|
|
if (database == NULL ||
|
|
database->connection == NULL ||
|
|
output_count == NULL ||
|
|
(capacity > 0U && output == NULL)) {
|
|
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
}
|
|
|
|
column = body_metric_column(metric);
|
|
if (column == NULL) {
|
|
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
}
|
|
|
|
written = snprintf(
|
|
sql,
|
|
sizeof(sql),
|
|
"SELECT observed_at, %s "
|
|
"FROM body_observations "
|
|
"WHERE %s IS NOT NULL "
|
|
"ORDER BY observed_at ASC, id ASC;",
|
|
column,
|
|
column
|
|
);
|
|
|
|
if (written < 0 ||
|
|
(size_t)written >= sizeof(sql)) {
|
|
return TRAINLOG_STATUS_SYSTEM_ERROR;
|
|
}
|
|
|
|
rc = sqlite3_prepare_v2(
|
|
database->connection,
|
|
sql,
|
|
-1,
|
|
&statement,
|
|
NULL
|
|
);
|
|
if (rc != SQLITE_OK) {
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
while ((rc = sqlite3_step(statement)) == SQLITE_ROW) {
|
|
if (count < capacity) {
|
|
const unsigned char *observed =
|
|
sqlite3_column_text(statement, 0);
|
|
|
|
if (observed == NULL) {
|
|
(void)sqlite3_finalize(statement);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
(void)snprintf(
|
|
output[count].observed_at,
|
|
sizeof(output[count].observed_at),
|
|
"%s",
|
|
(const char *)observed
|
|
);
|
|
|
|
output[count].value =
|
|
sqlite3_column_double(statement, 1);
|
|
}
|
|
|
|
++count;
|
|
}
|
|
|
|
if (rc != SQLITE_DONE) {
|
|
(void)sqlite3_finalize(statement);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
if (sqlite3_finalize(statement) != SQLITE_OK) {
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
*output_count =
|
|
count < capacity
|
|
? count
|
|
: capacity;
|
|
|
|
return TRAINLOG_STATUS_OK;
|
|
}
|
|
|
|
TrainlogStatus trainlog_database_latest_body_pair(
|
|
TrainlogDatabase *database,
|
|
TrainlogBodyMetric left_metric,
|
|
TrainlogBodyMetric right_metric,
|
|
TrainlogBodyPairPoint *output
|
|
)
|
|
{
|
|
const char *left_column;
|
|
const char *right_column;
|
|
char sql[384];
|
|
sqlite3_stmt *statement = NULL;
|
|
int written;
|
|
int rc;
|
|
|
|
if (database == NULL ||
|
|
database->connection == NULL ||
|
|
output == NULL) {
|
|
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
}
|
|
|
|
left_column = body_metric_column(left_metric);
|
|
right_column = body_metric_column(right_metric);
|
|
|
|
if (left_column == NULL ||
|
|
right_column == NULL) {
|
|
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
}
|
|
|
|
(void)memset(output, 0, sizeof(*output));
|
|
|
|
written = snprintf(
|
|
sql,
|
|
sizeof(sql),
|
|
"SELECT observed_at, %s, %s "
|
|
"FROM body_observations "
|
|
"WHERE %s IS NOT NULL AND %s IS NOT NULL "
|
|
"ORDER BY observed_at DESC, id DESC "
|
|
"LIMIT 1;",
|
|
left_column,
|
|
right_column,
|
|
left_column,
|
|
right_column
|
|
);
|
|
|
|
if (written < 0 ||
|
|
(size_t)written >= sizeof(sql)) {
|
|
return TRAINLOG_STATUS_SYSTEM_ERROR;
|
|
}
|
|
|
|
rc = sqlite3_prepare_v2(
|
|
database->connection,
|
|
sql,
|
|
-1,
|
|
&statement,
|
|
NULL
|
|
);
|
|
if (rc != SQLITE_OK) {
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
rc = sqlite3_step(statement);
|
|
|
|
if (rc == SQLITE_DONE) {
|
|
(void)sqlite3_finalize(statement);
|
|
output->found = false;
|
|
return TRAINLOG_STATUS_OK;
|
|
}
|
|
|
|
if (rc != SQLITE_ROW) {
|
|
(void)sqlite3_finalize(statement);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
{
|
|
const unsigned char *observed =
|
|
sqlite3_column_text(statement, 0);
|
|
|
|
if (observed == NULL) {
|
|
(void)sqlite3_finalize(statement);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
output->found = true;
|
|
|
|
(void)snprintf(
|
|
output->observed_at,
|
|
sizeof(output->observed_at),
|
|
"%s",
|
|
(const char *)observed
|
|
);
|
|
|
|
output->left_value =
|
|
sqlite3_column_double(statement, 1);
|
|
|
|
output->right_value =
|
|
sqlite3_column_double(statement, 2);
|
|
}
|
|
|
|
return sqlite3_finalize(statement) == SQLITE_OK
|
|
? TRAINLOG_STATUS_OK
|
|
: TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
/* TRAINLOG_EXERCISE_PERFORMANCE_IMPLEMENTATION */
|
|
|
|
static TrainlogLoadMode performance_load_mode_from_sql(
|
|
const char *text
|
|
)
|
|
{
|
|
if (text != NULL &&
|
|
strcmp(text, "external") == 0) {
|
|
return TRAINLOG_LOAD_EXTERNAL;
|
|
}
|
|
|
|
if (text != NULL &&
|
|
strcmp(text, "assistance") == 0) {
|
|
return TRAINLOG_LOAD_ASSISTANCE;
|
|
}
|
|
|
|
return TRAINLOG_LOAD_NONE;
|
|
}
|
|
|
|
static bool performance_candidate_better(
|
|
const TrainlogExercisePerformancePoint *current,
|
|
TrainlogLoadMode load_mode,
|
|
int metric_value,
|
|
int has_weight,
|
|
double weight_kg
|
|
)
|
|
{
|
|
if (current == NULL ||
|
|
metric_value <= 0) {
|
|
return false;
|
|
}
|
|
|
|
if (current->has_performance == 0) {
|
|
return true;
|
|
}
|
|
|
|
switch (load_mode) {
|
|
case TRAINLOG_LOAD_EXTERNAL:
|
|
if (has_weight == 0) {
|
|
return false;
|
|
}
|
|
|
|
if (weight_kg > current->weight_kg) {
|
|
return true;
|
|
}
|
|
|
|
return
|
|
weight_kg == current->weight_kg &&
|
|
metric_value > current->metric_value;
|
|
|
|
case TRAINLOG_LOAD_ASSISTANCE:
|
|
if (has_weight == 0) {
|
|
return false;
|
|
}
|
|
|
|
if (weight_kg < current->weight_kg) {
|
|
return true;
|
|
}
|
|
|
|
return
|
|
weight_kg == current->weight_kg &&
|
|
metric_value > current->metric_value;
|
|
|
|
case TRAINLOG_LOAD_NONE:
|
|
default:
|
|
return metric_value > current->metric_value;
|
|
}
|
|
}
|
|
|
|
/* TRAINLOG_MEASURED_MAX_SESSION_TYPE_V1 */
|
|
TrainlogStatus trainlog_database_list_exercise_performance(
|
|
TrainlogDatabase *database,
|
|
const char *exercise_id,
|
|
TrainlogExercisePerformancePoint *output,
|
|
size_t capacity,
|
|
size_t *output_count
|
|
)
|
|
{
|
|
static const char *const SQL =
|
|
"SELECT "
|
|
"s.session_id, "
|
|
"s.started_at, "
|
|
"s.session_type, "
|
|
"e.tracking_mode, "
|
|
"se.load_mode, "
|
|
"ps.id, "
|
|
"ps.reps, "
|
|
"ps.duration_seconds, "
|
|
"ps.weight_kg "
|
|
"FROM session_exercises AS se "
|
|
"JOIN sessions AS s "
|
|
" ON s.id = se.session_row_id "
|
|
"JOIN exercises AS e "
|
|
" ON e.id = se.exercise_row_id "
|
|
"LEFT JOIN performed_sets AS ps "
|
|
" ON ps.session_exercise_row_id = se.id "
|
|
"WHERE e.exercise_id = ?1 "
|
|
"ORDER BY "
|
|
"s.started_at DESC, "
|
|
"s.id DESC, "
|
|
"ps.position ASC;";
|
|
|
|
sqlite3_stmt *statement = NULL;
|
|
TrainlogExercisePerformancePoint *current = NULL;
|
|
char current_session_id[TRAINLOG_ID_MAX + 1U];
|
|
size_t copied = 0U;
|
|
int rc;
|
|
|
|
if (
|
|
database == NULL ||
|
|
database->connection == NULL ||
|
|
exercise_id == NULL ||
|
|
exercise_id[0] == '\0' ||
|
|
output_count == NULL ||
|
|
(
|
|
capacity > 0U &&
|
|
output == NULL
|
|
)
|
|
) {
|
|
return
|
|
TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
}
|
|
|
|
*output_count = 0U;
|
|
current_session_id[0] = '\0';
|
|
|
|
rc = sqlite3_prepare_v2(
|
|
database->connection,
|
|
SQL,
|
|
-1,
|
|
&statement,
|
|
NULL
|
|
);
|
|
|
|
if (rc != SQLITE_OK) {
|
|
return
|
|
TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
rc = sqlite3_bind_text(
|
|
statement,
|
|
1,
|
|
exercise_id,
|
|
-1,
|
|
SQLITE_TRANSIENT
|
|
);
|
|
|
|
if (rc != SQLITE_OK) {
|
|
(void)sqlite3_finalize(
|
|
statement
|
|
);
|
|
|
|
return
|
|
TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
while (
|
|
(rc = sqlite3_step(statement)) ==
|
|
SQLITE_ROW
|
|
) {
|
|
const unsigned char *session_id =
|
|
sqlite3_column_text(
|
|
statement,
|
|
0
|
|
);
|
|
|
|
const unsigned char *started_at =
|
|
sqlite3_column_text(
|
|
statement,
|
|
1
|
|
);
|
|
|
|
const unsigned char *session_type =
|
|
sqlite3_column_text(
|
|
statement,
|
|
2
|
|
);
|
|
|
|
const unsigned char *tracking_mode =
|
|
sqlite3_column_text(
|
|
statement,
|
|
3
|
|
);
|
|
|
|
const unsigned char *load_mode =
|
|
sqlite3_column_text(
|
|
statement,
|
|
4
|
|
);
|
|
|
|
bool new_session;
|
|
|
|
if (
|
|
session_id == NULL ||
|
|
started_at == NULL ||
|
|
session_type == NULL ||
|
|
tracking_mode == NULL ||
|
|
load_mode == NULL
|
|
) {
|
|
(void)sqlite3_finalize(
|
|
statement
|
|
);
|
|
|
|
return
|
|
TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
new_session =
|
|
current_session_id[0] == '\0' ||
|
|
strcmp(
|
|
current_session_id,
|
|
(const char *)session_id
|
|
) != 0;
|
|
|
|
if (new_session) {
|
|
(void)snprintf(
|
|
current_session_id,
|
|
sizeof(current_session_id),
|
|
"%s",
|
|
(const char *)session_id
|
|
);
|
|
|
|
current = NULL;
|
|
|
|
if (copied < capacity) {
|
|
current =
|
|
&output[copied];
|
|
|
|
(void)memset(
|
|
current,
|
|
0,
|
|
sizeof(*current)
|
|
);
|
|
|
|
(void)snprintf(
|
|
current->session_id,
|
|
sizeof(current->session_id),
|
|
"%s",
|
|
(const char *)session_id
|
|
);
|
|
|
|
(void)snprintf(
|
|
current->started_at,
|
|
sizeof(current->started_at),
|
|
"%s",
|
|
(const char *)started_at
|
|
);
|
|
|
|
if (
|
|
!session_type_from_sql(
|
|
(const char *)session_type,
|
|
¤t->session_type
|
|
)
|
|
) {
|
|
(void)sqlite3_finalize(
|
|
statement
|
|
);
|
|
|
|
return
|
|
TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
current->tracking_mode =
|
|
tracking_mode_from_sql(
|
|
(const char *)tracking_mode
|
|
);
|
|
|
|
current->load_mode =
|
|
performance_load_mode_from_sql(
|
|
(const char *)load_mode
|
|
);
|
|
|
|
++copied;
|
|
}
|
|
}
|
|
|
|
if (
|
|
current != NULL &&
|
|
sqlite3_column_type(
|
|
statement,
|
|
5
|
|
) != SQLITE_NULL
|
|
) {
|
|
int metric_value;
|
|
int has_weight;
|
|
double weight_kg;
|
|
|
|
++current->actual_set_count;
|
|
|
|
if (
|
|
current->tracking_mode ==
|
|
TRAINLOG_TRACKING_REPS
|
|
) {
|
|
metric_value =
|
|
sqlite3_column_type(
|
|
statement,
|
|
6
|
|
) != SQLITE_NULL
|
|
? sqlite3_column_int(
|
|
statement,
|
|
6
|
|
)
|
|
: 0;
|
|
} else {
|
|
metric_value =
|
|
sqlite3_column_type(
|
|
statement,
|
|
7
|
|
) != SQLITE_NULL
|
|
? sqlite3_column_int(
|
|
statement,
|
|
7
|
|
)
|
|
: 0;
|
|
}
|
|
|
|
has_weight =
|
|
sqlite3_column_type(
|
|
statement,
|
|
8
|
|
) != SQLITE_NULL;
|
|
|
|
weight_kg =
|
|
has_weight != 0
|
|
? sqlite3_column_double(
|
|
statement,
|
|
8
|
|
)
|
|
: 0.0;
|
|
|
|
if (
|
|
performance_candidate_better(
|
|
current,
|
|
current->load_mode,
|
|
metric_value,
|
|
has_weight,
|
|
weight_kg
|
|
)
|
|
) {
|
|
current->has_performance = 1;
|
|
current->metric_value =
|
|
metric_value;
|
|
current->has_weight =
|
|
has_weight;
|
|
current->weight_kg =
|
|
weight_kg;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (rc != SQLITE_DONE) {
|
|
(void)sqlite3_finalize(
|
|
statement
|
|
);
|
|
|
|
return
|
|
TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
if (
|
|
sqlite3_finalize(
|
|
statement
|
|
) != SQLITE_OK
|
|
) {
|
|
return
|
|
TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
*output_count = copied;
|
|
|
|
return TRAINLOG_STATUS_OK;
|
|
}
|
|
|
|
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
|
|
)
|
|
{
|
|
static const char *const HEADER_SQL =
|
|
"SELECT "
|
|
"started_at, "
|
|
"COALESCE(ended_at, ''), "
|
|
"session_type "
|
|
"FROM sessions "
|
|
"WHERE session_id = ?1;";
|
|
|
|
static const char *const EXERCISE_SQL =
|
|
"SELECT "
|
|
"se.id, "
|
|
"e.exercise_id, "
|
|
"e.name, "
|
|
"e.tracking_mode, "
|
|
"se.load_mode, "
|
|
"se.rest_seconds, "
|
|
"se.target_sets, "
|
|
"COALESCE(se.target_reps, 0), "
|
|
"COALESCE(se.target_duration_seconds, 0), "
|
|
"se.target_weight_kg, "
|
|
"COALESCE(se.notes, ''), "
|
|
"COALESCE(se.equipment_id, ''), se.entry_id "
|
|
"FROM session_exercises AS se "
|
|
"JOIN sessions AS s "
|
|
"ON s.id = se.session_row_id "
|
|
"JOIN exercises AS e "
|
|
"ON e.id = se.exercise_row_id "
|
|
"WHERE s.session_id = ?1 "
|
|
"ORDER BY se.position ASC;";
|
|
|
|
static const char *const SET_SQL =
|
|
"SELECT reps, duration_seconds, weight_kg "
|
|
"FROM performed_sets "
|
|
"WHERE session_exercise_row_id = ?1 "
|
|
"ORDER BY position ASC;";
|
|
|
|
sqlite3_stmt *header = NULL;
|
|
sqlite3_stmt *exercise_statement = NULL;
|
|
sqlite3_stmt *set_statement = NULL;
|
|
size_t exercise_count = 0U;
|
|
size_t set_count = 0U;
|
|
int rc;
|
|
|
|
if (database == NULL ||
|
|
database->connection == NULL ||
|
|
session_id == NULL ||
|
|
session_id[0] == '\0' ||
|
|
output_session == NULL ||
|
|
output_exercise_count == NULL ||
|
|
output_set_count == NULL ||
|
|
(exercise_capacity > 0U &&
|
|
output_exercises == NULL) ||
|
|
(set_capacity > 0U &&
|
|
output_sets == NULL)) {
|
|
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
}
|
|
|
|
*output_exercise_count = 0U;
|
|
*output_set_count = 0U;
|
|
(void)memset(
|
|
output_session,
|
|
0,
|
|
sizeof(*output_session)
|
|
);
|
|
|
|
rc = sqlite3_prepare_v2(
|
|
database->connection,
|
|
HEADER_SQL,
|
|
-1,
|
|
&header,
|
|
NULL
|
|
);
|
|
|
|
if (rc == SQLITE_OK) {
|
|
rc = sqlite3_bind_text(
|
|
header,
|
|
1,
|
|
session_id,
|
|
-1,
|
|
SQLITE_TRANSIENT
|
|
);
|
|
}
|
|
|
|
if (rc != SQLITE_OK) {
|
|
(void)sqlite3_finalize(header);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
rc = sqlite3_step(header);
|
|
|
|
if (rc == SQLITE_DONE) {
|
|
(void)sqlite3_finalize(header);
|
|
return TRAINLOG_STATUS_NOT_FOUND;
|
|
}
|
|
|
|
if (rc != SQLITE_ROW) {
|
|
(void)sqlite3_finalize(header);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
{
|
|
const unsigned char *started =
|
|
sqlite3_column_text(header, 0);
|
|
|
|
const unsigned char *ended =
|
|
sqlite3_column_text(header, 1);
|
|
|
|
const unsigned char *session_type =
|
|
sqlite3_column_text(header, 2);
|
|
|
|
if (started == NULL ||
|
|
ended == NULL ||
|
|
session_type == NULL) {
|
|
(void)sqlite3_finalize(header);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
(void)snprintf(
|
|
output_session->session_id,
|
|
sizeof(output_session->session_id),
|
|
"%s",
|
|
session_id
|
|
);
|
|
|
|
(void)snprintf(
|
|
output_session->started_at,
|
|
sizeof(output_session->started_at),
|
|
"%s",
|
|
(const char *)started
|
|
);
|
|
|
|
(void)snprintf(
|
|
output_session->ended_at,
|
|
sizeof(output_session->ended_at),
|
|
"%s",
|
|
(const char *)ended
|
|
);
|
|
|
|
if (!session_type_from_sql(
|
|
(const char *)session_type,
|
|
&output_session->session_type
|
|
)) {
|
|
(void)sqlite3_finalize(header);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
}
|
|
|
|
if (sqlite3_finalize(header) != SQLITE_OK) {
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
header = NULL;
|
|
|
|
rc = sqlite3_prepare_v2(
|
|
database->connection,
|
|
EXERCISE_SQL,
|
|
-1,
|
|
&exercise_statement,
|
|
NULL
|
|
);
|
|
|
|
if (rc == SQLITE_OK) {
|
|
rc = sqlite3_bind_text(
|
|
exercise_statement,
|
|
1,
|
|
session_id,
|
|
-1,
|
|
SQLITE_TRANSIENT
|
|
);
|
|
}
|
|
|
|
if (rc != SQLITE_OK) {
|
|
(void)sqlite3_finalize(exercise_statement);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
while ((rc = sqlite3_step(exercise_statement)) == SQLITE_ROW) {
|
|
TrainlogEditableExerciseRecord *record;
|
|
sqlite3_int64 session_exercise_row_id;
|
|
const unsigned char *exercise_id;
|
|
const unsigned char *name;
|
|
const unsigned char *tracking;
|
|
const unsigned char *load;
|
|
const unsigned char *notes;
|
|
int note_bytes;
|
|
|
|
if (exercise_count >= exercise_capacity) {
|
|
(void)sqlite3_finalize(exercise_statement);
|
|
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
}
|
|
|
|
record =
|
|
&output_exercises[exercise_count];
|
|
|
|
session_exercise_row_id =
|
|
sqlite3_column_int64(
|
|
exercise_statement,
|
|
0
|
|
);
|
|
|
|
exercise_id =
|
|
sqlite3_column_text(
|
|
exercise_statement,
|
|
1
|
|
);
|
|
|
|
name =
|
|
sqlite3_column_text(
|
|
exercise_statement,
|
|
2
|
|
);
|
|
|
|
tracking =
|
|
sqlite3_column_text(
|
|
exercise_statement,
|
|
3
|
|
);
|
|
|
|
load =
|
|
sqlite3_column_text(
|
|
exercise_statement,
|
|
4
|
|
);
|
|
|
|
notes =
|
|
sqlite3_column_text(
|
|
exercise_statement,
|
|
10
|
|
);
|
|
|
|
note_bytes =
|
|
sqlite3_column_bytes(
|
|
exercise_statement,
|
|
10
|
|
);
|
|
|
|
if (exercise_id == NULL ||
|
|
name == NULL ||
|
|
tracking == NULL ||
|
|
load == NULL ||
|
|
notes == NULL ||
|
|
note_bytes < 0 ||
|
|
(size_t)note_bytes >
|
|
TRAINLOG_NOTE_MAX) {
|
|
(void)sqlite3_finalize(exercise_statement);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
(void)memset(
|
|
record,
|
|
0,
|
|
sizeof(*record)
|
|
);
|
|
|
|
if (sqlite3_column_type(exercise_statement, 11) != SQLITE_NULL) {
|
|
const unsigned char *equipment_id = sqlite3_column_text(exercise_statement, 11);
|
|
if (equipment_id == NULL) {
|
|
(void)sqlite3_finalize(exercise_statement);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
(void)snprintf(record->equipment_id, sizeof(record->equipment_id),
|
|
"%s", (const char *)equipment_id);
|
|
}
|
|
|
|
if (sqlite3_column_type(exercise_statement, 12) == SQLITE_NULL) {
|
|
(void)sqlite3_finalize(exercise_statement);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
(void)snprintf(record->entry_id, sizeof(record->entry_id), "%s",
|
|
(const char *)sqlite3_column_text(exercise_statement, 12));
|
|
|
|
(void)snprintf(
|
|
record->exercise_id,
|
|
sizeof(record->exercise_id),
|
|
"%s",
|
|
(const char *)exercise_id
|
|
);
|
|
|
|
(void)snprintf(
|
|
record->name,
|
|
sizeof(record->name),
|
|
"%s",
|
|
(const char *)name
|
|
);
|
|
|
|
record->tracking_mode =
|
|
tracking_mode_from_sql(
|
|
(const char *)tracking
|
|
);
|
|
|
|
record->load_mode =
|
|
detail_load_mode_from_text(
|
|
(const char *)load
|
|
);
|
|
|
|
record->rest_seconds =
|
|
sqlite3_column_int(
|
|
exercise_statement,
|
|
5
|
|
);
|
|
|
|
record->target_sets =
|
|
sqlite3_column_int(
|
|
exercise_statement,
|
|
6
|
|
);
|
|
|
|
record->target_reps =
|
|
sqlite3_column_int(
|
|
exercise_statement,
|
|
7
|
|
);
|
|
|
|
record->target_duration_seconds =
|
|
sqlite3_column_int(
|
|
exercise_statement,
|
|
8
|
|
);
|
|
|
|
record->has_target_weight =
|
|
sqlite3_column_type(
|
|
exercise_statement,
|
|
9
|
|
) != SQLITE_NULL;
|
|
|
|
record->target_weight_kg =
|
|
record->has_target_weight != 0
|
|
? sqlite3_column_double(
|
|
exercise_statement,
|
|
9
|
|
)
|
|
: 0.0;
|
|
|
|
(void)snprintf(
|
|
record->notes,
|
|
sizeof(record->notes),
|
|
"%s",
|
|
(const char *)notes
|
|
);
|
|
|
|
record->set_offset = set_count;
|
|
|
|
rc = sqlite3_prepare_v2(
|
|
database->connection,
|
|
SET_SQL,
|
|
-1,
|
|
&set_statement,
|
|
NULL
|
|
);
|
|
|
|
if (rc == SQLITE_OK) {
|
|
rc = sqlite3_bind_int64(
|
|
set_statement,
|
|
1,
|
|
session_exercise_row_id
|
|
);
|
|
}
|
|
|
|
if (rc != SQLITE_OK) {
|
|
(void)sqlite3_finalize(set_statement);
|
|
(void)sqlite3_finalize(exercise_statement);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
while ((rc = sqlite3_step(set_statement)) == SQLITE_ROW) {
|
|
TrainlogSetInput *set;
|
|
int has_reps;
|
|
int has_duration;
|
|
|
|
if (set_count >= set_capacity) {
|
|
(void)sqlite3_finalize(set_statement);
|
|
(void)sqlite3_finalize(exercise_statement);
|
|
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
}
|
|
|
|
set = &output_sets[set_count];
|
|
|
|
has_reps =
|
|
sqlite3_column_type(
|
|
set_statement,
|
|
0
|
|
) != SQLITE_NULL;
|
|
|
|
has_duration =
|
|
sqlite3_column_type(
|
|
set_statement,
|
|
1
|
|
) != SQLITE_NULL;
|
|
|
|
if (has_reps == has_duration) {
|
|
(void)sqlite3_finalize(set_statement);
|
|
(void)sqlite3_finalize(exercise_statement);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
(void)memset(
|
|
set,
|
|
0,
|
|
sizeof(*set)
|
|
);
|
|
|
|
if (has_reps != 0) {
|
|
set->reps =
|
|
sqlite3_column_int(
|
|
set_statement,
|
|
0
|
|
);
|
|
} else {
|
|
set->duration_seconds =
|
|
sqlite3_column_int(
|
|
set_statement,
|
|
1
|
|
);
|
|
}
|
|
|
|
set->has_weight =
|
|
sqlite3_column_type(
|
|
set_statement,
|
|
2
|
|
) != SQLITE_NULL;
|
|
|
|
set->weight_kg =
|
|
set->has_weight
|
|
? sqlite3_column_double(
|
|
set_statement,
|
|
2
|
|
)
|
|
: 0.0;
|
|
|
|
++set_count;
|
|
++record->set_count;
|
|
}
|
|
|
|
if (rc != SQLITE_DONE) {
|
|
(void)sqlite3_finalize(set_statement);
|
|
(void)sqlite3_finalize(exercise_statement);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
if (sqlite3_finalize(set_statement) != SQLITE_OK) {
|
|
(void)sqlite3_finalize(exercise_statement);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
set_statement = NULL;
|
|
++exercise_count;
|
|
}
|
|
|
|
if (rc != SQLITE_DONE) {
|
|
(void)sqlite3_finalize(exercise_statement);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
if (sqlite3_finalize(exercise_statement) != SQLITE_OK) {
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
output_session->exercise_count =
|
|
exercise_count;
|
|
|
|
*output_exercise_count =
|
|
exercise_count;
|
|
|
|
*output_set_count =
|
|
set_count;
|
|
|
|
return TRAINLOG_STATUS_OK;
|
|
}
|
|
|
|
/* TRAINLOG_BODY_OBSERVATION_RECORD_IMPLEMENTATION */
|
|
|
|
static bool body_record_fill(
|
|
sqlite3_stmt *statement,
|
|
TrainlogBodyObservationRecord *record
|
|
)
|
|
{
|
|
const unsigned char *observation_id;
|
|
const unsigned char *observed_at;
|
|
const unsigned char *session_id;
|
|
const unsigned char *notes;
|
|
int note_bytes;
|
|
|
|
if (statement == NULL ||
|
|
record == NULL) {
|
|
return false;
|
|
}
|
|
|
|
observation_id =
|
|
sqlite3_column_text(statement, 0);
|
|
|
|
observed_at =
|
|
sqlite3_column_text(statement, 1);
|
|
|
|
session_id =
|
|
sqlite3_column_text(statement, 2);
|
|
|
|
notes =
|
|
sqlite3_column_text(statement, 17);
|
|
|
|
note_bytes =
|
|
sqlite3_column_bytes(statement, 17);
|
|
|
|
if (observation_id == NULL ||
|
|
observed_at == NULL ||
|
|
session_id == NULL ||
|
|
notes == NULL ||
|
|
note_bytes < 0 ||
|
|
(size_t)note_bytes > TRAINLOG_NOTE_MAX) {
|
|
return false;
|
|
}
|
|
|
|
(void)memset(
|
|
record,
|
|
0,
|
|
sizeof(*record)
|
|
);
|
|
|
|
(void)snprintf(
|
|
record->observation_id,
|
|
sizeof(record->observation_id),
|
|
"%s",
|
|
(const char *)observation_id
|
|
);
|
|
|
|
(void)snprintf(
|
|
record->observed_at,
|
|
sizeof(record->observed_at),
|
|
"%s",
|
|
(const char *)observed_at
|
|
);
|
|
|
|
(void)snprintf(
|
|
record->session_id,
|
|
sizeof(record->session_id),
|
|
"%s",
|
|
(const char *)session_id
|
|
);
|
|
|
|
#define READ_BODY_VALUE(column_, flag_, value_) \
|
|
do { \
|
|
(flag_) = \
|
|
sqlite3_column_type( \
|
|
statement, \
|
|
(column_) \
|
|
) != SQLITE_NULL; \
|
|
(value_) = \
|
|
(flag_) \
|
|
? sqlite3_column_double( \
|
|
statement, \
|
|
(column_) \
|
|
) \
|
|
: 0.0; \
|
|
} while (0)
|
|
|
|
READ_BODY_VALUE(
|
|
3,
|
|
record->has_body_weight,
|
|
record->body_weight_kg
|
|
);
|
|
|
|
READ_BODY_VALUE(
|
|
4,
|
|
record->has_neck,
|
|
record->neck_cm
|
|
);
|
|
|
|
READ_BODY_VALUE(
|
|
5,
|
|
record->has_shoulders,
|
|
record->shoulders_cm
|
|
);
|
|
|
|
READ_BODY_VALUE(
|
|
6,
|
|
record->has_chest,
|
|
record->chest_cm
|
|
);
|
|
|
|
READ_BODY_VALUE(
|
|
7,
|
|
record->has_waist,
|
|
record->waist_cm
|
|
);
|
|
|
|
READ_BODY_VALUE(
|
|
8,
|
|
record->has_hips,
|
|
record->hips_cm
|
|
);
|
|
|
|
READ_BODY_VALUE(
|
|
9,
|
|
record->has_left_arm,
|
|
record->left_arm_cm
|
|
);
|
|
|
|
READ_BODY_VALUE(
|
|
10,
|
|
record->has_right_arm,
|
|
record->right_arm_cm
|
|
);
|
|
|
|
READ_BODY_VALUE(
|
|
11,
|
|
record->has_left_forearm,
|
|
record->left_forearm_cm
|
|
);
|
|
|
|
READ_BODY_VALUE(
|
|
12,
|
|
record->has_right_forearm,
|
|
record->right_forearm_cm
|
|
);
|
|
|
|
READ_BODY_VALUE(
|
|
13,
|
|
record->has_left_thigh,
|
|
record->left_thigh_cm
|
|
);
|
|
|
|
READ_BODY_VALUE(
|
|
14,
|
|
record->has_right_thigh,
|
|
record->right_thigh_cm
|
|
);
|
|
|
|
READ_BODY_VALUE(
|
|
15,
|
|
record->has_left_calf,
|
|
record->left_calf_cm
|
|
);
|
|
|
|
READ_BODY_VALUE(
|
|
16,
|
|
record->has_right_calf,
|
|
record->right_calf_cm
|
|
);
|
|
|
|
#undef READ_BODY_VALUE
|
|
|
|
(void)snprintf(
|
|
record->notes,
|
|
sizeof(record->notes),
|
|
"%s",
|
|
(const char *)notes
|
|
);
|
|
|
|
return true;
|
|
}
|
|
|
|
static const char *const BODY_RECORD_SELECT =
|
|
"SELECT "
|
|
"bo.observation_id, "
|
|
"bo.observed_at, "
|
|
"COALESCE(s.session_id, ''), "
|
|
"bo.body_weight_kg, "
|
|
"bo.neck_cm, "
|
|
"bo.shoulders_cm, "
|
|
"bo.chest_cm, "
|
|
"bo.waist_cm, "
|
|
"bo.hips_cm, "
|
|
"bo.left_arm_cm, "
|
|
"bo.right_arm_cm, "
|
|
"bo.left_forearm_cm, "
|
|
"bo.right_forearm_cm, "
|
|
"bo.left_thigh_cm, "
|
|
"bo.right_thigh_cm, "
|
|
"bo.left_calf_cm, "
|
|
"bo.right_calf_cm, "
|
|
"COALESCE(bo.notes, '') "
|
|
"FROM body_observations AS bo "
|
|
"LEFT JOIN sessions AS s "
|
|
"ON s.id = bo.session_row_id ";
|
|
|
|
TrainlogStatus trainlog_database_list_body_observations(
|
|
TrainlogDatabase *database,
|
|
TrainlogBodyObservationRecord *output,
|
|
size_t capacity,
|
|
size_t *output_count
|
|
)
|
|
{
|
|
char sql[1024];
|
|
sqlite3_stmt *statement = NULL;
|
|
size_t count = 0U;
|
|
int written;
|
|
int rc;
|
|
|
|
if (database == NULL ||
|
|
database->connection == NULL ||
|
|
output_count == NULL ||
|
|
(capacity > 0U && output == NULL)) {
|
|
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
}
|
|
|
|
written = snprintf(
|
|
sql,
|
|
sizeof(sql),
|
|
"%s"
|
|
"ORDER BY bo.observed_at DESC, bo.id DESC;",
|
|
BODY_RECORD_SELECT
|
|
);
|
|
|
|
if (written < 0 ||
|
|
(size_t)written >= sizeof(sql)) {
|
|
return TRAINLOG_STATUS_SYSTEM_ERROR;
|
|
}
|
|
|
|
rc = sqlite3_prepare_v2(
|
|
database->connection,
|
|
sql,
|
|
-1,
|
|
&statement,
|
|
NULL
|
|
);
|
|
|
|
if (rc != SQLITE_OK) {
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
while ((rc = sqlite3_step(statement)) == SQLITE_ROW) {
|
|
if (count < capacity) {
|
|
if (!body_record_fill(
|
|
statement,
|
|
&output[count]
|
|
)) {
|
|
(void)sqlite3_finalize(statement);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
}
|
|
|
|
++count;
|
|
}
|
|
|
|
if (rc != SQLITE_DONE) {
|
|
(void)sqlite3_finalize(statement);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
if (sqlite3_finalize(statement) != SQLITE_OK) {
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
*output_count =
|
|
count < capacity
|
|
? count
|
|
: capacity;
|
|
|
|
return TRAINLOG_STATUS_OK;
|
|
}
|
|
|
|
TrainlogStatus trainlog_database_get_body_observation(
|
|
TrainlogDatabase *database,
|
|
const char *observation_id,
|
|
TrainlogBodyObservationRecord *output
|
|
)
|
|
{
|
|
char sql[1024];
|
|
sqlite3_stmt *statement = NULL;
|
|
int written;
|
|
int rc;
|
|
|
|
if (database == NULL ||
|
|
database->connection == NULL ||
|
|
observation_id == NULL ||
|
|
observation_id[0] == '\0' ||
|
|
output == NULL) {
|
|
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
}
|
|
|
|
written = snprintf(
|
|
sql,
|
|
sizeof(sql),
|
|
"%s"
|
|
"WHERE bo.observation_id = ?1;",
|
|
BODY_RECORD_SELECT
|
|
);
|
|
|
|
if (written < 0 ||
|
|
(size_t)written >= sizeof(sql)) {
|
|
return TRAINLOG_STATUS_SYSTEM_ERROR;
|
|
}
|
|
|
|
rc = sqlite3_prepare_v2(
|
|
database->connection,
|
|
sql,
|
|
-1,
|
|
&statement,
|
|
NULL
|
|
);
|
|
|
|
if (rc == SQLITE_OK) {
|
|
rc = sqlite3_bind_text(
|
|
statement,
|
|
1,
|
|
observation_id,
|
|
-1,
|
|
SQLITE_TRANSIENT
|
|
);
|
|
}
|
|
|
|
if (rc != SQLITE_OK) {
|
|
(void)sqlite3_finalize(statement);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
rc = sqlite3_step(statement);
|
|
|
|
if (rc == SQLITE_DONE) {
|
|
(void)sqlite3_finalize(statement);
|
|
return TRAINLOG_STATUS_NOT_FOUND;
|
|
}
|
|
|
|
if (rc != SQLITE_ROW) {
|
|
(void)sqlite3_finalize(statement);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
if (!body_record_fill(
|
|
statement,
|
|
output
|
|
)) {
|
|
(void)sqlite3_finalize(statement);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
return sqlite3_finalize(statement) == SQLITE_OK
|
|
? TRAINLOG_STATUS_OK
|
|
: TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
TrainlogStatus trainlog_database_update_body_observation(
|
|
TrainlogDatabase *database,
|
|
const TrainlogBodyObservationInput *observation
|
|
)
|
|
{
|
|
static const char *const SQL =
|
|
"UPDATE body_observations SET "
|
|
"body_weight_kg = ?1, "
|
|
"neck_cm = ?2, "
|
|
"shoulders_cm = ?3, "
|
|
"chest_cm = ?4, "
|
|
"waist_cm = ?5, "
|
|
"hips_cm = ?6, "
|
|
"left_arm_cm = ?7, "
|
|
"right_arm_cm = ?8, "
|
|
"left_forearm_cm = ?9, "
|
|
"right_forearm_cm = ?10, "
|
|
"left_thigh_cm = ?11, "
|
|
"right_thigh_cm = ?12, "
|
|
"left_calf_cm = ?13, "
|
|
"right_calf_cm = ?14, "
|
|
"notes = ?15 "
|
|
"WHERE observation_id = ?16;";
|
|
|
|
sqlite3_stmt *statement = NULL;
|
|
bool any_metric;
|
|
int rc;
|
|
|
|
if (database == NULL ||
|
|
database->connection == NULL ||
|
|
observation == NULL ||
|
|
observation->observation_id[0] == '\0') {
|
|
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
}
|
|
|
|
any_metric =
|
|
observation->has_body_weight ||
|
|
observation->has_neck ||
|
|
observation->has_shoulders ||
|
|
observation->has_chest ||
|
|
observation->has_waist ||
|
|
observation->has_hips ||
|
|
observation->has_left_arm ||
|
|
observation->has_right_arm ||
|
|
observation->has_left_forearm ||
|
|
observation->has_right_forearm ||
|
|
observation->has_left_thigh ||
|
|
observation->has_right_thigh ||
|
|
observation->has_left_calf ||
|
|
observation->has_right_calf;
|
|
|
|
if (!any_metric) {
|
|
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
}
|
|
|
|
rc = sqlite3_prepare_v2(
|
|
database->connection,
|
|
SQL,
|
|
-1,
|
|
&statement,
|
|
NULL
|
|
);
|
|
|
|
if (rc != SQLITE_OK) {
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
#define UPDATE_BODY_VALUE(index_, flag_, value_) \
|
|
do { \
|
|
if (rc == SQLITE_OK) { \
|
|
rc = bind_optional_double( \
|
|
statement, \
|
|
(index_), \
|
|
(flag_), \
|
|
(value_) \
|
|
); \
|
|
} \
|
|
} while (0)
|
|
|
|
UPDATE_BODY_VALUE(
|
|
1,
|
|
observation->has_body_weight,
|
|
observation->body_weight_kg
|
|
);
|
|
|
|
UPDATE_BODY_VALUE(
|
|
2,
|
|
observation->has_neck,
|
|
observation->neck_cm
|
|
);
|
|
|
|
UPDATE_BODY_VALUE(
|
|
3,
|
|
observation->has_shoulders,
|
|
observation->shoulders_cm
|
|
);
|
|
|
|
UPDATE_BODY_VALUE(
|
|
4,
|
|
observation->has_chest,
|
|
observation->chest_cm
|
|
);
|
|
|
|
UPDATE_BODY_VALUE(
|
|
5,
|
|
observation->has_waist,
|
|
observation->waist_cm
|
|
);
|
|
|
|
UPDATE_BODY_VALUE(
|
|
6,
|
|
observation->has_hips,
|
|
observation->hips_cm
|
|
);
|
|
|
|
UPDATE_BODY_VALUE(
|
|
7,
|
|
observation->has_left_arm,
|
|
observation->left_arm_cm
|
|
);
|
|
|
|
UPDATE_BODY_VALUE(
|
|
8,
|
|
observation->has_right_arm,
|
|
observation->right_arm_cm
|
|
);
|
|
|
|
UPDATE_BODY_VALUE(
|
|
9,
|
|
observation->has_left_forearm,
|
|
observation->left_forearm_cm
|
|
);
|
|
|
|
UPDATE_BODY_VALUE(
|
|
10,
|
|
observation->has_right_forearm,
|
|
observation->right_forearm_cm
|
|
);
|
|
|
|
UPDATE_BODY_VALUE(
|
|
11,
|
|
observation->has_left_thigh,
|
|
observation->left_thigh_cm
|
|
);
|
|
|
|
UPDATE_BODY_VALUE(
|
|
12,
|
|
observation->has_right_thigh,
|
|
observation->right_thigh_cm
|
|
);
|
|
|
|
UPDATE_BODY_VALUE(
|
|
13,
|
|
observation->has_left_calf,
|
|
observation->left_calf_cm
|
|
);
|
|
|
|
UPDATE_BODY_VALUE(
|
|
14,
|
|
observation->has_right_calf,
|
|
observation->right_calf_cm
|
|
);
|
|
|
|
#undef UPDATE_BODY_VALUE
|
|
|
|
if (rc == SQLITE_OK) {
|
|
rc =
|
|
observation->notes != NULL &&
|
|
observation->notes[0] != '\0'
|
|
? sqlite3_bind_text(
|
|
statement,
|
|
15,
|
|
observation->notes,
|
|
-1,
|
|
SQLITE_TRANSIENT
|
|
)
|
|
: sqlite3_bind_null(
|
|
statement,
|
|
15
|
|
);
|
|
}
|
|
|
|
if (rc == SQLITE_OK) {
|
|
rc = sqlite3_bind_text(
|
|
statement,
|
|
16,
|
|
observation->observation_id,
|
|
-1,
|
|
SQLITE_TRANSIENT
|
|
);
|
|
}
|
|
|
|
if (rc != SQLITE_OK) {
|
|
(void)sqlite3_finalize(statement);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
rc = sqlite3_step(statement);
|
|
|
|
if (rc != SQLITE_DONE) {
|
|
(void)sqlite3_finalize(statement);
|
|
|
|
return rc == SQLITE_CONSTRAINT
|
|
? TRAINLOG_STATUS_CONFLICT
|
|
: TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
if (sqlite3_changes(
|
|
database->connection
|
|
) != 1) {
|
|
(void)sqlite3_finalize(statement);
|
|
return TRAINLOG_STATUS_NOT_FOUND;
|
|
}
|
|
|
|
return sqlite3_finalize(statement) == SQLITE_OK
|
|
? TRAINLOG_STATUS_OK
|
|
: TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|