// SPDX-License-Identifier: GPL-3.0-or-later /* * $Id: log.c,v 1.1 2026/05/08 03:23:59 asm Exp $ * * log.c - Food log for GNUtrition * * Copyright (C) 2026 Free Software Foundation, Inc. * * Author: Jason Self * Anton McClure */ #include "log.h" #include "i18n.h" #include #include #include #define INITIAL_CAPACITY 32 sqlite3 * log_open (const char *path) { sqlite3 *db; int rc; char *errmsg; rc = sqlite3_open (path, &db); if (rc != SQLITE_OK) { fprintf (stderr, _("gnutrition: cannot open log database '%s': %s\n"), path, sqlite3_errmsg (db)); sqlite3_close (db); return NULL; } /* Create the log table if it does not exist. */ rc = sqlite3_exec (db, "CREATE TABLE IF NOT EXISTS food_log (" " id INTEGER PRIMARY KEY AUTOINCREMENT," " food_code INTEGER NOT NULL," " description TEXT NOT NULL," " date TEXT NOT NULL," " servings REAL NOT NULL DEFAULT 1.0" ")", NULL, NULL, &errmsg); if (rc != SQLITE_OK) { fprintf (stderr, _("gnutrition: cannot create log table: %s\n"), errmsg); sqlite3_free (errmsg); sqlite3_close (db); return NULL; } /* Create the profile table if it does not exist. Only one row is ever stored (id = 1). */ rc = sqlite3_exec (db, "CREATE TABLE IF NOT EXISTS user_profile (" " id INTEGER PRIMARY KEY CHECK (id = 1)," " age_years INTEGER NOT NULL," " height_cm REAL NOT NULL," " weight_kg REAL NOT NULL," " activity_level INTEGER NOT NULL," " gender INTEGER NOT NULL," " calorie_target INTEGER NOT NULL" ")", NULL, NULL, &errmsg); if (rc != SQLITE_OK) { fprintf (stderr, _("gnutrition: cannot create profile table: %s\n"), errmsg); sqlite3_free (errmsg); sqlite3_close (db); return NULL; } return db; } void log_close (sqlite3 *db) { if (db) sqlite3_close (db); } int log_add (sqlite3 *db, int food_code, const char *description, const char *date, double servings) { sqlite3_stmt *stmt; int rc; rc = sqlite3_prepare_v2 (db, "INSERT INTO food_log (food_code, description, date, servings) " "VALUES (?1, ?2, ?3, ?4)", -1, &stmt, NULL); if (rc != SQLITE_OK) { fprintf (stderr, _("gnutrition: SQL error: %s\n"), sqlite3_errmsg (db)); return -1; } sqlite3_bind_int (stmt, 1, food_code); sqlite3_bind_text (stmt, 2, description, -1, SQLITE_TRANSIENT); sqlite3_bind_text (stmt, 3, date, -1, SQLITE_TRANSIENT); sqlite3_bind_double (stmt, 4, servings); rc = sqlite3_step (stmt); sqlite3_finalize (stmt); if (rc != SQLITE_DONE) { fprintf (stderr, _("gnutrition: cannot add log entry: %s\n"), sqlite3_errmsg (db)); return -1; } return 0; } int log_delete (sqlite3 *db, int id) { sqlite3_stmt *stmt; int rc; rc = sqlite3_prepare_v2 (db, "DELETE FROM food_log WHERE id = ?1", -1, &stmt, NULL); if (rc != SQLITE_OK) { fprintf (stderr, _("gnutrition: SQL error: %s\n"), sqlite3_errmsg (db)); return -1; } sqlite3_bind_int (stmt, 1, id); rc = sqlite3_step (stmt); sqlite3_finalize (stmt); if (rc != SQLITE_DONE) { fprintf (stderr, _("gnutrition: cannot delete log entry: %s\n"), sqlite3_errmsg (db)); return -1; } return 0; } int log_update (sqlite3 *db, int id, const char *date, double servings) { sqlite3_stmt *stmt; int rc; rc = sqlite3_prepare_v2 (db, "UPDATE food_log SET date = ?1, servings = ?2 WHERE id = ?3", -1, &stmt, NULL); if (rc != SQLITE_OK) { fprintf (stderr, _("gnutrition: SQL error: %s\n"), sqlite3_errmsg (db)); return -1; } sqlite3_bind_text (stmt, 1, date, -1, SQLITE_TRANSIENT); sqlite3_bind_double (stmt, 2, servings); sqlite3_bind_int (stmt, 3, id); rc = sqlite3_step (stmt); sqlite3_finalize (stmt); if (rc != SQLITE_DONE) { fprintf (stderr, _("gnutrition: cannot update log entry: %s\n"), sqlite3_errmsg (db)); return -1; } return 0; } /* Append a log_entry to LIST, growing dynamically. */ static int log_list_append (struct log_list *list, int id, int food_code, const char *description, const char *date, double servings) { if (list->count >= list->capacity) { size_t new_cap; struct log_entry *tmp; new_cap = list->capacity == 0 ? INITIAL_CAPACITY : list->capacity * 2; tmp = realloc (list->items, new_cap * sizeof *tmp); if (!tmp) { fprintf (stderr, _("gnutrition: memory exhausted\n")); return -1; } list->items = tmp; list->capacity = new_cap; } list->items[list->count].id = id; list->items[list->count].food_code = food_code; list->items[list->count].description = strdup (description); list->items[list->count].date = strdup (date); if (!list->items[list->count].description || !list->items[list->count].date) { fprintf (stderr, _("gnutrition: memory exhausted\n")); return -1; } list->items[list->count].servings = servings; list->count++; return 0; } int log_get_day (sqlite3 *db, const char *date, struct log_list *results) { sqlite3_stmt *stmt; int rc; results->items = NULL; results->count = 0; results->capacity = 0; rc = sqlite3_prepare_v2 (db, "SELECT id, food_code, description, date, servings " "FROM food_log " "WHERE date = ?1 " "ORDER BY id", -1, &stmt, NULL); if (rc != SQLITE_OK) { fprintf (stderr, _("gnutrition: SQL error: %s\n"), sqlite3_errmsg (db)); return -1; } sqlite3_bind_text (stmt, 1, date, -1, SQLITE_TRANSIENT); while ((rc = sqlite3_step (stmt)) == SQLITE_ROW) { int id = sqlite3_column_int (stmt, 0); int food_code = sqlite3_column_int (stmt, 1); const char *desc = (const char *) sqlite3_column_text (stmt, 2); const char *d = (const char *) sqlite3_column_text (stmt, 3); double servings = sqlite3_column_double (stmt, 4); if (log_list_append (results, id, food_code, desc ? desc : "", d ? d : "", servings) < 0) { sqlite3_finalize (stmt); log_list_free (results); return -1; } } sqlite3_finalize (stmt); if (rc != SQLITE_DONE) { fprintf (stderr, _("gnutrition: SQL error: %s\n"), sqlite3_errmsg (db)); log_list_free (results); return -1; } return 0; } int log_save_profile (sqlite3 *db, const struct user_profile *profile) { sqlite3_stmt *stmt; int rc; rc = sqlite3_prepare_v2 (db, "INSERT OR REPLACE INTO user_profile " "(id, age_years, height_cm, weight_kg, activity_level, gender, calorie_target) " "VALUES (1, ?1, ?2, ?3, ?4, ?5, ?6)", -1, &stmt, NULL); if (rc != SQLITE_OK) { fprintf (stderr, _("gnutrition: SQL error: %s\n"), sqlite3_errmsg (db)); return -1; } sqlite3_bind_int (stmt, 1, profile->age_years); sqlite3_bind_double (stmt, 2, profile->height_cm); sqlite3_bind_double (stmt, 3, profile->weight_kg); sqlite3_bind_int (stmt, 4, profile->activity_level); sqlite3_bind_int (stmt, 5, profile->gender); sqlite3_bind_int (stmt, 6, profile->calorie_target); rc = sqlite3_step (stmt); sqlite3_finalize (stmt); if (rc != SQLITE_DONE) { fprintf (stderr, _("gnutrition: cannot save profile: %s\n"), sqlite3_errmsg (db)); return -1; } return 0; } int log_get_profile (sqlite3 *db, struct user_profile *profile) { sqlite3_stmt *stmt; int rc; memset (profile, 0, sizeof *profile); rc = sqlite3_prepare_v2 (db, "SELECT age_years, height_cm, weight_kg, " "activity_level, gender, calorie_target " "FROM user_profile WHERE id = 1", -1, &stmt, NULL); if (rc != SQLITE_OK) { fprintf (stderr, _("gnutrition: SQL error: %s\n"), sqlite3_errmsg (db)); return -1; } rc = sqlite3_step (stmt); if (rc == SQLITE_ROW) { profile->age_years = sqlite3_column_int (stmt, 0); profile->height_cm = sqlite3_column_double (stmt, 1); profile->weight_kg = sqlite3_column_double (stmt, 2); profile->activity_level = sqlite3_column_int (stmt, 3); profile->gender = sqlite3_column_int (stmt, 4); profile->calorie_target = sqlite3_column_int (stmt, 5); sqlite3_finalize (stmt); return 0; } else if (rc == SQLITE_DONE) { sqlite3_finalize (stmt); return 1; /* No profile saved yet. */ } else { fprintf (stderr, _("gnutrition: SQL error: %s\n"), sqlite3_errmsg (db)); sqlite3_finalize (stmt); return -1; } } void log_list_free (struct log_list *list) { size_t i; if (!list) return; for (i = 0; i < list->count; i++) { free (list->items[i].description); free (list->items[i].date); } free (list->items); list->items = NULL; list->count = 0; list->capacity = 0; } int log_get_dates (sqlite3 *db, struct date_list *results) { sqlite3_stmt *stmt; int rc; results->dates = NULL; results->count = 0; results->capacity = 0; rc = sqlite3_prepare_v2 (db, "SELECT DISTINCT date FROM food_log ORDER BY date", -1, &stmt, NULL); if (rc != SQLITE_OK) { fprintf (stderr, _("gnutrition: SQL error: %s\n"), sqlite3_errmsg (db)); return -1; } while ((rc = sqlite3_step (stmt)) == SQLITE_ROW) { const char *d = (const char *) sqlite3_column_text (stmt, 0); if (results->count >= results->capacity) { size_t new_cap; char **tmp; new_cap = results->capacity == 0 ? INITIAL_CAPACITY : results->capacity * 2; tmp = realloc (results->dates, new_cap * sizeof *tmp); if (!tmp) { fprintf (stderr, _("gnutrition: memory exhausted\n")); sqlite3_finalize (stmt); date_list_free (results); return -1; } results->dates = tmp; results->capacity = new_cap; } results->dates[results->count] = strdup (d ? d : ""); if (!results->dates[results->count]) { fprintf (stderr, _("gnutrition: memory exhausted\n")); sqlite3_finalize (stmt); date_list_free (results); return -1; } results->count++; } sqlite3_finalize (stmt); if (rc != SQLITE_DONE) { fprintf (stderr, _("gnutrition: SQL error: %s\n"), sqlite3_errmsg (db)); date_list_free (results); return -1; } return 0; } void date_list_free (struct date_list *list) { size_t i; if (!list) return; for (i = 0; i < list->count; i++) free (list->dates[i]); free (list->dates); list->dates = NULL; list->count = 0; list->capacity = 0; }