Annotation of gnutrition/db.c, revision 1.1
1.1 ! asm 1: // SPDX-License-Identifier: GPL-3.0-or-later
! 2: /*
! 3: * $Id$
! 4: *
! 5: * db.c - Database access layer for GNUtrition
! 6: *
! 7: * Copyright (C) 2026 Free Software Foundation, Inc.
! 8: *
! 9: * Author: Jason Self <jself@gnu.org>
! 10: */
! 11:
! 12: #include "db.h"
! 13: #include "i18n.h"
! 14:
! 15: #include <stdio.h>
! 16: #include <stdlib.h>
! 17: #include <string.h>
! 18:
! 19: #define INITIAL_CAPACITY 64
! 20:
! 21: sqlite3 *
! 22: db_open (const char *path)
! 23: {
! 24: sqlite3 *db;
! 25: int rc;
! 26:
! 27: /* Note: We rely on SQLite's internal handling of mmap. SQLite attempts
! 28: to mmap and falls back to read/write if it fails, satisfying the
! 29: requirement to handle filesystems that don't support mmap. */
! 30: rc = sqlite3_open_v2 (path, &db, SQLITE_OPEN_READONLY, NULL);
! 31: if (rc != SQLITE_OK)
! 32: {
! 33: fprintf (stderr, _("gnutrition: cannot open database '%s': %s\n"),
! 34: path, sqlite3_errmsg (db));
! 35: sqlite3_close (db);
! 36: return NULL;
! 37: }
! 38: return db;
! 39: }
! 40:
! 41: void
! 42: db_close (sqlite3 *db)
! 43: {
! 44: if (db)
! 45: sqlite3_close (db);
! 46: }
! 47:
! 48: /* Append a food_item to LIST, growing dynamically. Returns 0 on
! 49: success, -1 on allocation failure. */
! 50: static int
! 51: food_list_append (struct food_list *list, int code, const char *desc)
! 52: {
! 53: if (list->count >= list->capacity)
! 54: {
! 55: size_t new_cap;
! 56: struct food_item *tmp;
! 57:
! 58: new_cap = list->capacity == 0 ? INITIAL_CAPACITY : list->capacity * 2;
! 59: tmp = realloc (list->items, new_cap * sizeof *tmp);
! 60: if (!tmp)
! 61: {
! 62: fprintf (stderr, _("gnutrition: memory exhausted\n"));
! 63: return -1;
! 64: }
! 65: list->items = tmp;
! 66: list->capacity = new_cap;
! 67: }
! 68:
! 69: list->items[list->count].food_code = code;
! 70: list->items[list->count].description = strdup (desc);
! 71: if (!list->items[list->count].description)
! 72: {
! 73: fprintf (stderr, _("gnutrition: memory exhausted\n"));
! 74: return -1;
! 75: }
! 76: list->count++;
! 77: return 0;
! 78: }
! 79:
! 80: int
! 81: db_search_foods (sqlite3 *db, const char *query, struct food_list *results)
! 82: {
! 83: sqlite3_stmt *stmt;
! 84: int rc;
! 85: char *pattern;
! 86: size_t qlen;
! 87:
! 88: results->items = NULL;
! 89: results->count = 0;
! 90: results->capacity = 0;
! 91:
! 92: /* Build a LIKE pattern: %query%. */
! 93: qlen = strlen (query);
! 94: pattern = malloc (qlen + 3);
! 95: if (!pattern)
! 96: {
! 97: fprintf (stderr, _("gnutrition: memory exhausted\n"));
! 98: return -1;
! 99: }
! 100: pattern[0] = '%';
! 101: memcpy (pattern + 1, query, qlen);
! 102: pattern[qlen + 1] = '%';
! 103: pattern[qlen + 2] = '\0';
! 104:
! 105: rc = sqlite3_prepare_v2 (db,
! 106: "SELECT \"Food code\", \"Main food description\" "
! 107: "FROM foods "
! 108: "WHERE \"Main food description\" LIKE ?1 "
! 109: "ORDER BY \"Main food description\" "
! 110: "LIMIT 100",
! 111: -1, &stmt, NULL);
! 112:
! 113: if (rc != SQLITE_OK)
! 114: {
! 115: fprintf (stderr, _("gnutrition: SQL error: %s\n"), sqlite3_errmsg (db));
! 116: free (pattern);
! 117: return -1;
! 118: }
! 119:
! 120: sqlite3_bind_text (stmt, 1, pattern, -1, SQLITE_TRANSIENT);
! 121:
! 122: while ((rc = sqlite3_step (stmt)) == SQLITE_ROW)
! 123: {
! 124: int code = sqlite3_column_int (stmt, 0);
! 125: const char *desc = (const char *) sqlite3_column_text (stmt, 1);
! 126: if (food_list_append (results, code, desc ? desc : "") < 0)
! 127: {
! 128: sqlite3_finalize (stmt);
! 129: free (pattern);
! 130: food_list_free (results);
! 131: return -1;
! 132: }
! 133: }
! 134:
! 135: sqlite3_finalize (stmt);
! 136: free (pattern);
! 137:
! 138: if (rc != SQLITE_DONE)
! 139: {
! 140: fprintf (stderr, _("gnutrition: SQL error: %s\n"), sqlite3_errmsg (db));
! 141: food_list_free (results);
! 142: return -1;
! 143: }
! 144:
! 145: return 0;
! 146: }
! 147:
! 148: /* Append a nutrient_info to LIST, growing dynamically. */
! 149: static int
! 150: nutrient_list_append (struct nutrient_list *list, const char *name,
! 151: double value)
! 152: {
! 153: if (list->count >= list->capacity)
! 154: {
! 155: size_t new_cap;
! 156: struct nutrient_info *tmp;
! 157:
! 158: new_cap = list->capacity == 0 ? INITIAL_CAPACITY : list->capacity * 2;
! 159: tmp = realloc (list->items, new_cap * sizeof *tmp);
! 160: if (!tmp)
! 161: {
! 162: fprintf (stderr, _("gnutrition: memory exhausted\n"));
! 163: return -1;
! 164: }
! 165: list->items = tmp;
! 166: list->capacity = new_cap;
! 167: }
! 168:
! 169: list->items[list->count].name = strdup (name);
! 170: if (!list->items[list->count].name)
! 171: {
! 172: fprintf (stderr, _("gnutrition: memory exhausted\n"));
! 173: return -1;
! 174: }
! 175: list->items[list->count].value = value;
! 176: list->count++;
! 177: return 0;
! 178: }
! 179:
! 180: /* Fatty acid shorthand-to-common-name mapping. The keys are the
! 181: cleaned column names (after newline removal) produced by the USDA
! 182: FNDDS import. */
! 183: static const struct { const char *key; const char *name; } fa_names[] =
! 184: {
! 185: { "4:0(g)", "Butanoic acid (4:0) (g)" },
! 186: { "6:0(g)", "Hexanoic acid (6:0) (g)" },
! 187: { "8:0(g)", "Octanoic acid (8:0) (g)" },
! 188: { "10:0(g)", "Decanoic acid (10:0) (g)" },
! 189: { "12:0(g)", "Dodecanoic acid (12:0) (g)" },
! 190: { "14:0(g)", "Tetradecanoic acid (14:0) (g)" },
! 191: { "16:0(g)", "Hexadecanoic acid (16:0) (g)" },
! 192: { "18:0(g)", "Octadecanoic acid (18:0) (g)" },
! 193: { "16:1(g)", "Hexadecenoic acid (16:1) (g)" },
! 194: { "18:1(g)", "Octadecenoic acid (18:1) (g)" },
! 195: { "20:1(g)", "Eicosenoic acid (20:1) (g)" },
! 196: { "22:1(g)", "Docosenoic acid (22:1) (g)" },
! 197: { "18:2(g)", "Octadecadienoic acid (18:2) (g)" },
! 198: { "18:3(g)", "Octadecatrienoic acid (18:3) (g)" },
! 199: { "18:4(g)", "Octadecatetraenoic acid (18:4) (g)" },
! 200: { "20:4(g)", "Eicosatetraenoic acid (20:4) (g)" },
! 201: { "20:5 n-3(g)", "EPA (20:5 n-3) (g)" },
! 202: { "22:5 n-3(g)", "DPA (22:5 n-3) (g)" },
! 203: { "22:6 n-3(g)", "DHA (22:6 n-3) (g)" },
! 204: };
! 205:
! 206: #define NUM_FA_NAMES (sizeof fa_names / sizeof fa_names[0])
! 207:
! 208: /* Build a clean nutrient name from a column name that may contain
! 209: embedded newlines (an artifact of multi-line Excel headers).
! 210: Fatty-acid shorthand codes are expanded to their common names. */
! 211: static char *
! 212: clean_column_name (const char *raw)
! 213: {
! 214: char *clean;
! 215: size_t i, j, len;
! 216:
! 217: len = strlen (raw);
! 218: clean = malloc (len + 1);
! 219: if (!clean)
! 220: {
! 221: fprintf (stderr, _("gnutrition: memory exhausted\n"));
! 222: return NULL;
! 223: }
! 224:
! 225: for (i = 0, j = 0; i < len; i++)
! 226: {
! 227: if (raw[i] == '\n')
! 228: continue;
! 229: clean[j++] = raw[i];
! 230: }
! 231: clean[j] = '\0';
! 232:
! 233: /* Replace fatty acid shorthand with a readable name. */
! 234: for (i = 0; i < NUM_FA_NAMES; i++)
! 235: {
! 236: if (strcmp (clean, fa_names[i].key) == 0)
! 237: {
! 238: char *expanded = strdup (fa_names[i].name);
! 239: if (!expanded)
! 240: {
! 241: fprintf (stderr, _("gnutrition: memory exhausted\n"));
! 242: free (clean);
! 243: return NULL;
! 244: }
! 245: free (clean);
! 246: return expanded;
! 247: }
! 248: }
! 249:
! 250: return clean;
! 251: }
! 252:
! 253: int
! 254: db_get_nutrients (sqlite3 *db, int food_code, struct nutrient_list *results)
! 255: {
! 256: sqlite3_stmt *stmt;
! 257: int rc, col, ncols;
! 258:
! 259: results->items = NULL;
! 260: results->count = 0;
! 261: results->capacity = 0;
! 262:
! 263: /* The nutrients table stores each nutrient as a separate column
! 264: (wide format). Columns 0-3 are metadata (Food code, description,
! 265: WWEIA category number/description); nutrient data starts at
! 266: column 4. */
! 267: rc = sqlite3_prepare_v2 (db,
! 268: "SELECT * FROM nutrients WHERE \"Food code\" = ?1",
! 269: -1, &stmt, NULL);
! 270:
! 271: if (rc != SQLITE_OK)
! 272: {
! 273: fprintf (stderr, _("gnutrition: SQL error: %s\n"), sqlite3_errmsg (db));
! 274: return -1;
! 275: }
! 276:
! 277: sqlite3_bind_int (stmt, 1, food_code);
! 278: rc = sqlite3_step (stmt);
! 279:
! 280: if (rc == SQLITE_ROW)
! 281: {
! 282: ncols = sqlite3_column_count (stmt);
! 283: for (col = 4; col < ncols; col++)
! 284: {
! 285: const char *raw_name = sqlite3_column_name (stmt, col);
! 286: char *name = clean_column_name (raw_name ? raw_name : "");
! 287: double value = sqlite3_column_double (stmt, col);
! 288: if (!name
! 289: || nutrient_list_append (results, name, value) < 0)
! 290: {
! 291: free (name);
! 292: sqlite3_finalize (stmt);
! 293: nutrient_list_free (results);
! 294: return -1;
! 295: }
! 296: free (name);
! 297: }
! 298: }
! 299: else if (rc != SQLITE_DONE)
! 300: {
! 301: fprintf (stderr, _("gnutrition: SQL error: %s\n"), sqlite3_errmsg (db));
! 302: sqlite3_finalize (stmt);
! 303: return -1;
! 304: }
! 305:
! 306: sqlite3_finalize (stmt);
! 307: return 0;
! 308: }
! 309:
! 310: int
! 311: db_get_fped (sqlite3 *db, int food_code, struct fped_entry *entry)
! 312: {
! 313: sqlite3_stmt *stmt;
! 314: int rc;
! 315:
! 316: memset (entry, 0, sizeof *entry);
! 317: entry->food_code = food_code;
! 318:
! 319: /* The FPED table columns include aggregated food pattern categories.
! 320: We sum the sub-categories into the six major groups. */
! 321: rc = sqlite3_prepare_v2 (db,
! 322: "SELECT "
! 323: " COALESCE(\"V_TOTAL (cup eq)\", 0), "
! 324: " COALESCE(\"F_TOTAL (cup eq)\", 0), "
! 325: " COALESCE(\"G_TOTAL (oz eq)\", 0), "
! 326: " COALESCE(\"D_TOTAL (cup eq)\", 0), "
! 327: " COALESCE(\"PF_TOTAL (oz eq)\", 0), "
! 328: " COALESCE(\"OILS (grams)\", 0) "
! 329: "FROM points "
! 330: "WHERE FOODCODE = ?1 "
! 331: "LIMIT 1",
! 332: -1, &stmt, NULL);
! 333:
! 334: if (rc != SQLITE_OK)
! 335: {
! 336: fprintf (stderr, _("gnutrition: SQL error: %s\n"), sqlite3_errmsg (db));
! 337: return -1;
! 338: }
! 339:
! 340: sqlite3_bind_int (stmt, 1, food_code);
! 341: rc = sqlite3_step (stmt);
! 342:
! 343: if (rc == SQLITE_ROW)
! 344: {
! 345: entry->vegetables = sqlite3_column_double (stmt, 0);
! 346: entry->fruits = sqlite3_column_double (stmt, 1);
! 347: entry->grains = sqlite3_column_double (stmt, 2);
! 348: entry->dairy = sqlite3_column_double (stmt, 3);
! 349: entry->protein = sqlite3_column_double (stmt, 4);
! 350: entry->oils = sqlite3_column_double (stmt, 5);
! 351: sqlite3_finalize (stmt);
! 352: return 0;
! 353: }
! 354: else if (rc == SQLITE_DONE)
! 355: {
! 356: sqlite3_finalize (stmt);
! 357: return 1; /* Not found. */
! 358: }
! 359: else
! 360: {
! 361: fprintf (stderr, _("gnutrition: SQL error: %s\n"), sqlite3_errmsg (db));
! 362: sqlite3_finalize (stmt);
! 363: return -1;
! 364: }
! 365: }
! 366:
! 367: void
! 368: food_list_free (struct food_list *list)
! 369: {
! 370: size_t i;
! 371: if (!list)
! 372: return;
! 373: for (i = 0; i < list->count; i++)
! 374: free (list->items[i].description);
! 375: free (list->items);
! 376: list->items = NULL;
! 377: list->count = 0;
! 378: list->capacity = 0;
! 379: }
! 380:
! 381: void
! 382: nutrient_list_free (struct nutrient_list *list)
! 383: {
! 384: size_t i;
! 385: if (!list)
! 386: return;
! 387: for (i = 0; i < list->count; i++)
! 388: free (list->items[i].name);
! 389: free (list->items);
! 390: list->items = NULL;
! 391: list->count = 0;
! 392: list->capacity = 0;
! 393: }
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>