// SPDX-License-Identifier: GPL-3.0-or-later
/*
* $Id: db.c,v 1.1 2026/05/08 03:23:58 asm Exp $
*
* db.c - Database access layer for GNUtrition
*
* Copyright (C) 2026 Free Software Foundation, Inc.
*
* Author: Jason Self <jself@gnu.org>
*/
#include "db.h"
#include "i18n.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define INITIAL_CAPACITY 64
sqlite3 *
db_open (const char *path)
{
sqlite3 *db;
int rc;
/* Note: We rely on SQLite's internal handling of mmap. SQLite attempts
to mmap and falls back to read/write if it fails, satisfying the
requirement to handle filesystems that don't support mmap. */
rc = sqlite3_open_v2 (path, &db, SQLITE_OPEN_READONLY, NULL);
if (rc != SQLITE_OK)
{
fprintf (stderr, _("gnutrition: cannot open database '%s': %s\n"),
path, sqlite3_errmsg (db));
sqlite3_close (db);
return NULL;
}
return db;
}
void
db_close (sqlite3 *db)
{
if (db)
sqlite3_close (db);
}
/* Append a food_item to LIST, growing dynamically. Returns 0 on
success, -1 on allocation failure. */
static int
food_list_append (struct food_list *list, int code, const char *desc)
{
if (list->count >= list->capacity)
{
size_t new_cap;
struct food_item *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].food_code = code;
list->items[list->count].description = strdup (desc);
if (!list->items[list->count].description)
{
fprintf (stderr, _("gnutrition: memory exhausted\n"));
return -1;
}
list->count++;
return 0;
}
int
db_search_foods (sqlite3 *db, const char *query, struct food_list *results)
{
sqlite3_stmt *stmt;
int rc;
char *pattern;
size_t qlen;
results->items = NULL;
results->count = 0;
results->capacity = 0;
/* Build a LIKE pattern: %query%. */
qlen = strlen (query);
pattern = malloc (qlen + 3);
if (!pattern)
{
fprintf (stderr, _("gnutrition: memory exhausted\n"));
return -1;
}
pattern[0] = '%';
memcpy (pattern + 1, query, qlen);
pattern[qlen + 1] = '%';
pattern[qlen + 2] = '\0';
rc = sqlite3_prepare_v2 (db,
"SELECT \"Food code\", \"Main food description\" "
"FROM foods "
"WHERE \"Main food description\" LIKE ?1 "
"ORDER BY \"Main food description\" "
"LIMIT 100",
-1, &stmt, NULL);
if (rc != SQLITE_OK)
{
fprintf (stderr, _("gnutrition: SQL error: %s\n"), sqlite3_errmsg (db));
free (pattern);
return -1;
}
sqlite3_bind_text (stmt, 1, pattern, -1, SQLITE_TRANSIENT);
while ((rc = sqlite3_step (stmt)) == SQLITE_ROW)
{
int code = sqlite3_column_int (stmt, 0);
const char *desc = (const char *) sqlite3_column_text (stmt, 1);
if (food_list_append (results, code, desc ? desc : "") < 0)
{
sqlite3_finalize (stmt);
free (pattern);
food_list_free (results);
return -1;
}
}
sqlite3_finalize (stmt);
free (pattern);
if (rc != SQLITE_DONE)
{
fprintf (stderr, _("gnutrition: SQL error: %s\n"), sqlite3_errmsg (db));
food_list_free (results);
return -1;
}
return 0;
}
/* Append a nutrient_info to LIST, growing dynamically. */
static int
nutrient_list_append (struct nutrient_list *list, const char *name,
double value)
{
if (list->count >= list->capacity)
{
size_t new_cap;
struct nutrient_info *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].name = strdup (name);
if (!list->items[list->count].name)
{
fprintf (stderr, _("gnutrition: memory exhausted\n"));
return -1;
}
list->items[list->count].value = value;
list->count++;
return 0;
}
/* Fatty acid shorthand-to-common-name mapping. The keys are the
cleaned column names (after newline removal) produced by the USDA
FNDDS import. */
static const struct { const char *key; const char *name; } fa_names[] =
{
{ "4:0(g)", "Butanoic acid (4:0) (g)" },
{ "6:0(g)", "Hexanoic acid (6:0) (g)" },
{ "8:0(g)", "Octanoic acid (8:0) (g)" },
{ "10:0(g)", "Decanoic acid (10:0) (g)" },
{ "12:0(g)", "Dodecanoic acid (12:0) (g)" },
{ "14:0(g)", "Tetradecanoic acid (14:0) (g)" },
{ "16:0(g)", "Hexadecanoic acid (16:0) (g)" },
{ "18:0(g)", "Octadecanoic acid (18:0) (g)" },
{ "16:1(g)", "Hexadecenoic acid (16:1) (g)" },
{ "18:1(g)", "Octadecenoic acid (18:1) (g)" },
{ "20:1(g)", "Eicosenoic acid (20:1) (g)" },
{ "22:1(g)", "Docosenoic acid (22:1) (g)" },
{ "18:2(g)", "Octadecadienoic acid (18:2) (g)" },
{ "18:3(g)", "Octadecatrienoic acid (18:3) (g)" },
{ "18:4(g)", "Octadecatetraenoic acid (18:4) (g)" },
{ "20:4(g)", "Eicosatetraenoic acid (20:4) (g)" },
{ "20:5 n-3(g)", "EPA (20:5 n-3) (g)" },
{ "22:5 n-3(g)", "DPA (22:5 n-3) (g)" },
{ "22:6 n-3(g)", "DHA (22:6 n-3) (g)" },
};
#define NUM_FA_NAMES (sizeof fa_names / sizeof fa_names[0])
/* Build a clean nutrient name from a column name that may contain
embedded newlines (an artifact of multi-line Excel headers).
Fatty-acid shorthand codes are expanded to their common names. */
static char *
clean_column_name (const char *raw)
{
char *clean;
size_t i, j, len;
len = strlen (raw);
clean = malloc (len + 1);
if (!clean)
{
fprintf (stderr, _("gnutrition: memory exhausted\n"));
return NULL;
}
for (i = 0, j = 0; i < len; i++)
{
if (raw[i] == '\n')
continue;
clean[j++] = raw[i];
}
clean[j] = '\0';
/* Replace fatty acid shorthand with a readable name. */
for (i = 0; i < NUM_FA_NAMES; i++)
{
if (strcmp (clean, fa_names[i].key) == 0)
{
char *expanded = strdup (fa_names[i].name);
if (!expanded)
{
fprintf (stderr, _("gnutrition: memory exhausted\n"));
free (clean);
return NULL;
}
free (clean);
return expanded;
}
}
return clean;
}
int
db_get_nutrients (sqlite3 *db, int food_code, struct nutrient_list *results)
{
sqlite3_stmt *stmt;
int rc, col, ncols;
results->items = NULL;
results->count = 0;
results->capacity = 0;
/* The nutrients table stores each nutrient as a separate column
(wide format). Columns 0-3 are metadata (Food code, description,
WWEIA category number/description); nutrient data starts at
column 4. */
rc = sqlite3_prepare_v2 (db,
"SELECT * FROM nutrients WHERE \"Food code\" = ?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, food_code);
rc = sqlite3_step (stmt);
if (rc == SQLITE_ROW)
{
ncols = sqlite3_column_count (stmt);
for (col = 4; col < ncols; col++)
{
const char *raw_name = sqlite3_column_name (stmt, col);
char *name = clean_column_name (raw_name ? raw_name : "");
double value = sqlite3_column_double (stmt, col);
if (!name
|| nutrient_list_append (results, name, value) < 0)
{
free (name);
sqlite3_finalize (stmt);
nutrient_list_free (results);
return -1;
}
free (name);
}
}
else if (rc != SQLITE_DONE)
{
fprintf (stderr, _("gnutrition: SQL error: %s\n"), sqlite3_errmsg (db));
sqlite3_finalize (stmt);
return -1;
}
sqlite3_finalize (stmt);
return 0;
}
int
db_get_fped (sqlite3 *db, int food_code, struct fped_entry *entry)
{
sqlite3_stmt *stmt;
int rc;
memset (entry, 0, sizeof *entry);
entry->food_code = food_code;
/* The FPED table columns include aggregated food pattern categories.
We sum the sub-categories into the six major groups. */
rc = sqlite3_prepare_v2 (db,
"SELECT "
" COALESCE(\"V_TOTAL (cup eq)\", 0), "
" COALESCE(\"F_TOTAL (cup eq)\", 0), "
" COALESCE(\"G_TOTAL (oz eq)\", 0), "
" COALESCE(\"D_TOTAL (cup eq)\", 0), "
" COALESCE(\"PF_TOTAL (oz eq)\", 0), "
" COALESCE(\"OILS (grams)\", 0) "
"FROM points "
"WHERE FOODCODE = ?1 "
"LIMIT 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, food_code);
rc = sqlite3_step (stmt);
if (rc == SQLITE_ROW)
{
entry->vegetables = sqlite3_column_double (stmt, 0);
entry->fruits = sqlite3_column_double (stmt, 1);
entry->grains = sqlite3_column_double (stmt, 2);
entry->dairy = sqlite3_column_double (stmt, 3);
entry->protein = sqlite3_column_double (stmt, 4);
entry->oils = sqlite3_column_double (stmt, 5);
sqlite3_finalize (stmt);
return 0;
}
else if (rc == SQLITE_DONE)
{
sqlite3_finalize (stmt);
return 1; /* Not found. */
}
else
{
fprintf (stderr, _("gnutrition: SQL error: %s\n"), sqlite3_errmsg (db));
sqlite3_finalize (stmt);
return -1;
}
}
void
food_list_free (struct food_list *list)
{
size_t i;
if (!list)
return;
for (i = 0; i < list->count; i++)
free (list->items[i].description);
free (list->items);
list->items = NULL;
list->count = 0;
list->capacity = 0;
}
void
nutrient_list_free (struct nutrient_list *list)
{
size_t i;
if (!list)
return;
for (i = 0; i < list->count; i++)
free (list->items[i].name);
free (list->items);
list->items = NULL;
list->count = 0;
list->capacity = 0;
}
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>