/** * @file database.c * @brief SQLite persistence implementation for Trainlog. */ #include "trainlog/database.h" #include "trainlog/duration.h" #include #include #include #include #include struct TrainlogDatabase { sqlite3 *connection; }; static const char *const SCHEMA_V5_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," " 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)" " )" ");" "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_V5_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 = 5;" "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 ) { int version = 0; TrainlogStatus status; status = trainlog_database_schema_version( database, &version ); if ( status != TRAINLOG_STATUS_OK ) { return status; } if ( version > TRAINLOG_DATABASE_SCHEMA_VERSION ) { return TRAINLOG_STATUS_SCHEMA_UNSUPPORTED; } if ( version == TRAINLOG_DATABASE_SCHEMA_VERSION ) { return TRAINLOG_STATUS_OK; } if (version == 0) { status = execute_sql( database, SCHEMA_V5_SQL_A ); if ( status == TRAINLOG_STATUS_OK ) { status = execute_sql( database, SCHEMA_V5_SQL_B ); } } 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 { 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 ) { (void)sqlite3_exec( database->connection, "ROLLBACK;", NULL, NULL, NULL ); } return status; } TrainlogStatus trainlog_database_open( const char *path, TrainlogDatabase **output_database ) { TrainlogDatabase *database; int rc; TrainlogStatus status; if (path == NULL || path[0] == '\0' || output_database == NULL) { return TRAINLOG_STATUS_INVALID_ARGUMENT; } *output_database = NULL; database = calloc(1U, sizeof(*database)); if (database == NULL) { 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) { trainlog_database_close(database); return TRAINLOG_STATUS_DATABASE_ERROR; } if (sqlite3_busy_timeout(database->connection, 5000) != SQLITE_OK) { trainlog_database_close(database); return TRAINLOG_STATUS_DATABASE_ERROR; } status = execute_sql(database, "PRAGMA foreign_keys = ON;"); if (status != TRAINLOG_STATUS_OK) { trainlog_database_close(database); return status; } status = initialize_or_validate_schema(database); 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_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; 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; } 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(" "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" ") VALUES(" "?1, ?2, ?3, ?4, ?5, ?6, " "?7, ?8, ?9, ?10, ?11, ?12" ");", -1, &statement, NULL ); if (rc != SQLITE_OK) { return TRAINLOG_STATUS_DATABASE_ERROR; } rc = sqlite3_bind_int64( statement, 1, session_row_id ); if (rc == SQLITE_OK) { rc = sqlite3_bind_int64( statement, 2, exercise_row_id ); } if (rc == SQLITE_OK) { rc = sqlite3_bind_text( statement, 3, recording_mode, -1, SQLITE_STATIC ); } if (rc == SQLITE_OK) { rc = sqlite3_bind_int64( statement, 4, (sqlite3_int64)input->data_fields ); } if (rc == SQLITE_OK) { rc = sqlite3_bind_int64( statement, 5, (sqlite3_int64)position ); } if (rc == SQLITE_OK) { rc = sqlite3_bind_text( statement, 6, load_mode, -1, SQLITE_STATIC ); } if (rc == SQLITE_OK) { rc = sqlite3_bind_int( statement, 7, input->rest_seconds ); } if (rc == SQLITE_OK) { rc = input->target_sets > 0 ? sqlite3_bind_int( statement, 8, input->target_sets ) : sqlite3_bind_null( statement, 8 ); } if (rc == SQLITE_OK) { rc = input->target_reps > 0 ? sqlite3_bind_int( statement, 9, input->target_reps ) : sqlite3_bind_null( statement, 9 ); } if (rc == SQLITE_OK) { rc = input->target_duration_seconds > 0 ? sqlite3_bind_int( statement, 10, input->target_duration_seconds ) : sqlite3_bind_null( statement, 10 ); } if (rc == SQLITE_OK) { rc = input->target_has_weight ? sqlite3_bind_double( statement, 11, input->target_weight_kg ) : sqlite3_bind_null( statement, 11 ); } if (rc == SQLITE_OK) { rc = input->notes != NULL && input->notes[0] != '\0' ? sqlite3_bind_text( statement, 12, input->notes, -1, SQLITE_TRANSIENT ) : sqlite3_bind_null( statement, 12 ); } 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.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, 13); 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) ); (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, '') " "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) ); (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; }