2024-09-18 15:31:47 +02:00
/*
* Copyright (C) 2024 Nicolás Ortega Froysa <nicolas@ortegas.org>
* Nicolás Ortega Froysa <nicolas@ortegas.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
2024-10-09 19:50:53 +02:00
#include "db.hpp"
2024-09-18 15:31:47 +02:00
2024-10-09 21:12:16 +02:00
#include <cstdlib>
#include <filesystem>
#include <format>
2024-10-11 17:43:28 +02:00
#include <iostream>
#include <sqlite3.h>
#include <stdexcept>
2024-09-18 15:31:47 +02:00
2024-10-09 21:12:16 +02:00
#define DB_VERSION 1
2024-09-18 15:31:47 +02:00
2024-10-11 17:43:28 +02:00
void db :: open ( void ) {
2024-10-09 21:12:16 +02:00
std :: string xdg_data_home ;
std :: string db_path ;
bool new_db = false ;
2024-09-18 15:31:47 +02:00
2024-10-11 17:43:28 +02:00
if (( xdg_data_home = std :: getenv ( "XDG_DATA_HOME" )). empty ())
throw std :: runtime_error ( "Cannot find environment variable XDG_DATA_HOME. Please define it before continuing." );
2024-09-18 15:31:47 +02:00
2024-10-09 21:12:16 +02:00
db_path = xdg_data_home + "/menu-helper" ;
2024-09-18 15:31:47 +02:00
2024-10-09 21:12:16 +02:00
if ( not std :: filesystem :: exists ( db_path ))
std :: filesystem :: create_directories ( db_path );
2024-09-24 18:15:37 +02:00
2024-10-09 21:12:16 +02:00
db_path += "/recipes.db" ;
2024-09-24 18:15:37 +02:00
2024-10-09 21:12:16 +02:00
if ( not std :: filesystem :: exists ( db_path )) {
2024-10-21 14:30:16 +02:00
std :: cout << "Creating database in " << db_path << std :: endl ;
2024-10-09 21:12:16 +02:00
new_db = true ;
2024-09-18 15:31:47 +02:00
}
2024-10-11 17:43:28 +02:00
if ( sqlite3_open ( db_path . c_str (), & sqlite_db ) not_eq SQLITE_OK )
throw std :: runtime_error ( "Failed to open database file " + db_path );
if ( new_db ) {
2024-10-12 10:57:47 +02:00
sqlite3_exec ( sqlite_db , "CREATE TABLE db_version(version INTEGER UNIQUE NOT NULL);" , nullptr , nullptr , nullptr );
2024-10-11 17:43:28 +02:00
sqlite3_exec ( sqlite_db , std :: format ( "INSERT INTO db_version VALUES({});" , DB_VERSION ). c_str (), nullptr , nullptr , nullptr );
sqlite3_exec ( sqlite_db , "CREATE TABLE tags(id INTEGER PRIMARY KEY AUTOINCREMENT, name STRING UNIQUE);" , nullptr , nullptr , nullptr );
sqlite3_exec ( sqlite_db , "CREATE TABLE ingredients(id INTEGER PRIMARY KEY AUTOINCREMENT, name STRING UNIQUE);" , nullptr , nullptr , nullptr );
sqlite3_exec ( sqlite_db , "CREATE TABLE recipes(id INTEGER PRIMARY KEY AUTOINCREMENT, name STRING UNIQUE, description STRING);" , nullptr , nullptr , nullptr );
2024-10-12 11:02:56 +02:00
sqlite3_exec ( sqlite_db , "CREATE TABLE recipe_tag(recipe_id INTEGER REFERENCES recipes(id) ON DELETE CASCADE, tag_id INTEGER REFERENCES tags(id) ON DELETE CASCADE, UNIQUE(recipe_id, tag_id));" , nullptr , nullptr , nullptr );
sqlite3_exec ( sqlite_db , "CREATE TABLE recipe_ingredient(recipe_id INTEGER REFERENCES recipes(id) ON DELETE CASCADE, ingredient_id INTEGER REFERENCES ingredients(id) ON DELETE CASCADE, UNIQUE(recipe_id, ingredient_id));" , nullptr , nullptr , nullptr );
2024-09-18 15:31:47 +02:00
}
}
2024-10-11 17:43:28 +02:00
void db :: close ( void ) {
if ( not sqlite_db )
2024-09-18 15:31:47 +02:00
return ;
2024-10-11 17:43:28 +02:00
sqlite3_close ( sqlite_db );
sqlite_db = nullptr ;
2024-09-18 15:31:47 +02:00
}
2024-09-24 18:15:37 +02:00
int query_id_cb ( void * recipe_id_var , int col_num , char ** col_data , char ** col_name ) {
int * recipe_id_ptr = ( int * ) recipe_id_var ;
int ret = 1 ;
for ( int i = 0 ; i < col_num ; ++ i ) {
2024-10-09 21:12:16 +02:00
if ( std :: string ( col_name [ i ]) == "id" ) {
* recipe_id_ptr = std :: atoi ( col_data [ i ]);
2024-09-24 18:15:37 +02:00
ret = 0 ;
break ;
}
}
return ret ;
}
2024-10-11 17:43:28 +02:00
int db :: table_get_id_by_name ( const std :: string & table , const std :: string & name ) {
2024-09-24 18:15:37 +02:00
int id = 0 ;
2024-10-11 17:43:28 +02:00
if ( not sqlite_db )
throw std :: runtime_error ( std :: format ( "{}: Database not open! Please contact a developer." , __PRETTY_FUNCTION__ ));
if ( sqlite3_exec ( sqlite_db , std :: format ( "SELECT id FROM {} WHERE lower(name)=lower('{}');" , table , name ). c_str (),
query_id_cb , & id , nullptr ) not_eq SQLITE_OK ) {
throw std :: runtime_error ( std :: format ( "Failed to get ID of '{}' from table '{}'." , name , table ));
}
2024-09-24 18:15:37 +02:00
return id ;
}
2024-10-11 17:43:28 +02:00
int db :: add_recipe ( const std :: string & name , const std :: string & description ) {
if ( not sqlite_db )
throw std :: runtime_error ( std :: format ( "{}: Database not open! Please contact a developer." , __PRETTY_FUNCTION__ ));
2024-09-24 18:15:37 +02:00
2024-10-21 13:54:07 +02:00
if ( sqlite3_exec ( sqlite_db , std :: format ( "INSERT OR IGNORE INTO recipes(name,description) VALUES('{}','{}');" , name , description ). c_str (),
2024-10-11 17:43:28 +02:00
nullptr , nullptr , nullptr ) not_eq SQLITE_OK ) {
throw std :: runtime_error ( "Failed to insert new recipe into database." );
}
2024-09-24 18:15:37 +02:00
2024-10-11 17:43:28 +02:00
return get_recipe_id ( name );
2024-09-24 18:15:37 +02:00
}
2024-10-11 17:43:28 +02:00
void db :: del_recipe ( const int id ) {
if ( not sqlite_db )
throw std :: runtime_error ( std :: format ( "{}: Database not open! Please contact a developer." , __PRETTY_FUNCTION__ ));
2024-10-11 10:43:38 +02:00
2024-10-11 17:43:28 +02:00
if ( sqlite3_exec ( sqlite_db , std :: format ( "DELETE FROM recipes WHERE id={}" , id ). c_str (),
nullptr , nullptr , nullptr ) not_eq SQLITE_OK ) {
throw std :: runtime_error ( std :: format ( "Failed to delete recipe with ID {} from database." , id ));
}
2024-10-11 10:43:38 +02:00
}
2024-10-11 17:43:28 +02:00
void db :: del_recipes ( const std :: vector < int > & ids ) {
2024-10-11 10:43:38 +02:00
std :: string stmt = "DELETE FROM recipes WHERE id IN (" ;
2024-10-11 17:43:28 +02:00
if ( not sqlite_db )
throw std :: runtime_error ( std :: format ( "{}: Database not open! Please contact a developer." , __PRETTY_FUNCTION__ ));
2024-10-11 10:43:38 +02:00
bool first = true ;
for ( auto id : ids ) {
if ( first )
first = false ;
else
stmt += "," ;
stmt += std :: to_string ( id );
}
stmt += ");" ;
2024-10-11 17:43:28 +02:00
if ( sqlite3_exec ( sqlite_db , stmt . c_str (), nullptr , nullptr , nullptr ) not_eq SQLITE_OK )
throw std :: runtime_error ( "Failed to delete recipes from database." );
2024-10-11 10:43:38 +02:00
}
2024-10-11 17:43:28 +02:00
bool db :: recipe_exists ( const int id ) {
2024-10-11 10:43:38 +02:00
bool exists = false ;
2024-10-11 17:43:28 +02:00
if ( not sqlite_db )
throw std :: runtime_error ( std :: format ( "{}: Database not open! Please contact a developer." , __PRETTY_FUNCTION__ ));
2024-10-11 10:43:38 +02:00
2024-10-11 17:43:28 +02:00
if ( sqlite3_exec ( sqlite_db , std :: format ( "SELECT id FROM recipes WHERE id={}" , id ). c_str (),
[]( void * found , int , char ** , char ** ) {
* static_cast < bool *> ( found ) = true ;
return 0 ;
}, & exists , nullptr ) not_eq SQLITE_OK ) {
throw std :: runtime_error ( "Failed to select from database." );
}
2024-10-11 10:43:38 +02:00
return exists ;
}
2024-10-11 17:43:28 +02:00
struct recipe db :: get_recipe ( const int id ) {
2024-10-11 12:01:22 +02:00
struct recipe recipe ;
2024-10-11 17:43:28 +02:00
if ( not sqlite_db )
throw std :: runtime_error ( std :: format ( "{}: Database not open! Please contact a developer." , __PRETTY_FUNCTION__ ));
if ( sqlite3_exec ( sqlite_db , std :: format ( "SELECT * FROM recipes WHERE id={};" , id ). c_str (),
[]( void * recipe , int , char ** col_data , char ** ) {
* static_cast < struct recipe *> ( recipe ) = { std :: atoi ( col_data [ 0 ]), col_data [ 1 ], col_data [ 2 ] };
return 0 ;
}, & recipe , nullptr ) not_eq SQLITE_OK ) {
throw std :: runtime_error ( "Failed to select from database." );
}
2024-10-11 12:01:22 +02:00
return recipe ;
}
2024-10-21 18:37:53 +02:00
void db :: update_recipe_name ( const int id , const std :: string & new_name ) {
if ( not sqlite_db )
throw std :: runtime_error ( std :: format ( "{}: Database not open! Please contact a developer." , __PRETTY_FUNCTION__ ));
if ( sqlite3_exec ( sqlite_db , std :: format ( "UPDATE OR IGNORE recipes SET name='{}' WHERE id={};" , new_name , id ). c_str (),
nullptr , nullptr , nullptr ) not_eq SQLITE_OK ) {
throw std :: runtime_error ( std :: format ( "Failed to modify name of recipe with ID {}." , id ));
}
}
2024-10-21 18:44:38 +02:00
void db :: update_recipe_desc ( const int id , const std :: string & new_desc ) {
if ( not sqlite_db )
throw std :: runtime_error ( std :: format ( "{}: Database not open! Please contact a developer." , __PRETTY_FUNCTION__ ));
if ( sqlite3_exec ( sqlite_db , std :: format ( "UPDATE OR IGNORE recipes SET description='{}' WHERE id={};" , new_desc , id ). c_str (),
nullptr , nullptr , nullptr ) not_eq SQLITE_OK ) {
throw std :: runtime_error ( std :: format ( "Failed to modify description of recipe with ID {}." , id ));
}
}
2024-10-11 17:43:28 +02:00
std :: vector < struct recipe > db :: get_recipes ( const std :: vector < std :: string > & ingredients ,
const std :: vector < std :: string > & tags )
2024-10-11 09:24:48 +02:00
{
std :: vector < struct recipe > recipes ;
std :: string stmt = "SELECT id,name,description FROM recipes" ;
std :: string filters ;
2024-10-11 17:43:28 +02:00
if ( not sqlite_db )
throw std :: runtime_error ( std :: format ( "{}: Database not open! Please contact a developer." , __PRETTY_FUNCTION__ ));
2024-10-11 09:24:48 +02:00
if ( not ingredients . empty () or not tags . empty ())
filters += " WHERE" ;
if ( not ingredients . empty ()) {
bool first = true ;
for ( auto & i : ingredients ) {
int id ;
if ( first )
first = false ;
else
filters += " AND" ;
filters += " id IN (SELECT recipe_id FROM recipe_ingredient WHERE ingredient_id=" ;
2024-10-11 17:43:28 +02:00
if (( id = get_ingredient_id ( i )) <= 0 )
throw std :: runtime_error ( std :: format ( "Failed to find ingredient '{}'" , i ));
2024-10-11 09:24:48 +02:00
2024-10-11 17:43:28 +02:00
filters += std :: to_string ( id );
2024-10-11 09:24:48 +02:00
filters += ")" ;
}
}
if ( not tags . empty ()) {
if ( not filters . empty ())
filters += " AND" ;
bool first = true ;
for ( auto & i : tags ) {
int id ;
if ( first )
first = false ;
else
filters += " AND" ;
filters += " id IN (SELECT recipe_id FROM recipe_tag WHERE tag_id=" ;
2024-10-11 17:43:28 +02:00
if (( id = get_tag_id ( i )) <= 0 )
throw std :: runtime_error ( "Failed to find tag '{}'" );
2024-10-11 09:24:48 +02:00
2024-10-11 17:43:28 +02:00
filters += std :: to_string ( id );
2024-10-11 09:24:48 +02:00
filters += ")" ;
}
}
stmt += filters + ";" ;
2024-10-11 17:43:28 +02:00
if ( sqlite3_exec ( sqlite_db , stmt . c_str (),
[]( void * recipe_list , int , char ** col_data , char ** ) {
static_cast < std :: vector < struct recipe >*> ( recipe_list ) -> push_back ({
std :: atoi ( col_data [ 0 ]),
col_data [ 1 ],
col_data [ 2 ] });
2024-10-11 09:24:48 +02:00
return 0 ;
2024-10-11 17:43:28 +02:00
}, & recipes , nullptr ) not_eq SQLITE_OK ) {
throw std :: runtime_error ( "Failed to select recipes." );
}
2024-10-11 09:24:48 +02:00
return recipes ;
}
2024-10-11 17:43:28 +02:00
int db :: add_ingredient ( const std :: string & name ) {
if ( not sqlite_db )
throw std :: runtime_error ( std :: format ( "{}: Database not open! Please contact a developer." , __PRETTY_FUNCTION__ ));
2024-09-24 18:15:37 +02:00
2024-10-21 13:54:07 +02:00
if ( sqlite3_exec ( sqlite_db , std :: format ( "INSERT OR IGNORE INTO ingredients(name) VALUES(lower('{}'));" , name ). c_str (),
2024-10-11 17:43:28 +02:00
nullptr , nullptr , nullptr ) not_eq SQLITE_OK ) {
throw std :: runtime_error ( std :: format ( "Failed to instert ingredient '{}'." , name ));
}
2024-09-24 18:15:37 +02:00
2024-10-11 17:43:28 +02:00
return get_ingredient_id ( name );
2024-09-24 18:15:37 +02:00
}
2024-10-11 17:43:28 +02:00
std :: vector < std :: string > db :: get_recipe_ingredients ( const int id ) {
2024-10-11 12:01:22 +02:00
std :: vector < std :: string > ingredients ;
2024-10-11 17:43:28 +02:00
if ( not sqlite_db )
throw std :: runtime_error ( std :: format ( "{}: Database not open! Please contact a developer." , __PRETTY_FUNCTION__ ));
2024-10-11 12:01:22 +02:00
2024-10-11 17:43:28 +02:00
if ( sqlite3_exec ( sqlite_db , std :: format ( "SELECT name FROM ingredients WHERE id IN (SELECT ingredient_id FROM recipe_ingredient WHERE recipe_id={});" , id ). c_str (),
[]( void * ingredients , int , char ** col_data , char ** ) {
static_cast < std :: vector < std :: string >*> ( ingredients ) -> push_back ( col_data [ 0 ]);
return 0 ;
}, & ingredients , nullptr ) not_eq SQLITE_OK ) {
throw std :: runtime_error ( std :: format ( "Failed to select ingredients from recipe with ID {}" , id ));
}
2024-09-24 18:15:37 +02:00
2024-10-11 17:43:28 +02:00
return ingredients ;
2024-09-24 18:15:37 +02:00
}
2024-10-11 17:43:28 +02:00
int db :: add_tag ( const std :: string & name ) {
if ( not sqlite_db )
throw std :: runtime_error ( std :: format ( "{}: Database not open! Please contact a developer." , __PRETTY_FUNCTION__ ));
2024-09-24 18:15:37 +02:00
2024-10-21 13:54:07 +02:00
if ( sqlite3_exec ( sqlite_db , std :: format ( "INSERT OR IGNORE INTO tags(name) VALUES('{}');" , name ). c_str (),
2024-10-11 17:43:28 +02:00
nullptr , nullptr , nullptr ) not_eq SQLITE_OK ) {
throw std :: runtime_error ( std :: format ( "Failed to insert tag '{}'" , name ));
}
2024-09-24 18:15:37 +02:00
2024-10-11 17:43:28 +02:00
return get_tag_id ( name );
2024-09-24 18:15:37 +02:00
}
2024-10-11 17:43:28 +02:00
std :: vector < std :: string > db :: get_recipe_tags ( const int id ) {
2024-10-11 12:01:22 +02:00
std :: vector < std :: string > tags ;
2024-10-11 17:43:28 +02:00
if ( not sqlite_db )
throw std :: runtime_error ( std :: format ( "{}: Database not open! Please contact a developer." , __PRETTY_FUNCTION__ ));
2024-10-11 12:01:22 +02:00
2024-10-11 17:43:28 +02:00
if ( sqlite3_exec ( sqlite_db , std :: format ( "SELECT name FROM tags WHERE id IN (SELECT tag_id FROM recipe_tag WHERE recipe_id={});" , id ). c_str (),
[]( void * tags , int , char ** col_data , char ** ) {
static_cast < std :: vector < std :: string >*> ( tags ) -> push_back ( col_data [ 0 ]);
return 0 ;
}, & tags , nullptr ) not_eq SQLITE_OK ) {
throw std :: runtime_error ( std :: format ( "Failed to select tags for recipe with ID {}" , id ));
}
2024-09-24 18:15:37 +02:00
2024-10-11 17:43:28 +02:00
return tags ;
2024-09-24 18:15:37 +02:00
}
2024-10-21 14:05:26 +02:00
void db :: conn_recipe_ingredient ( const int recipe_id , const int ingredient_id ) {
2024-10-11 17:43:28 +02:00
if ( not sqlite_db )
throw std :: runtime_error ( std :: format ( "{}: Database not open! Please contact a developer." , __PRETTY_FUNCTION__ ));
2024-09-24 18:15:37 +02:00
2024-10-21 13:54:07 +02:00
if ( sqlite3_exec ( sqlite_db , std :: format ( "INSERT OR IGNORE INTO recipe_ingredient(recipe_id, ingredient_id) VALUES({},{});" , recipe_id , ingredient_id ). c_str (),
2024-10-11 17:43:28 +02:00
nullptr , nullptr , nullptr ) not_eq SQLITE_OK ) {
throw std :: runtime_error ( std :: format ( "Failed to connect recipe with ID {} to ingredient with ID {}" ,
recipe_id , ingredient_id ));
}
2024-09-24 18:15:37 +02:00
}
2024-10-21 14:05:26 +02:00
void db :: disconn_recipe_ingredient ( const int recipe_id , const int ingredient_id ) {
2024-10-21 13:54:07 +02:00
if ( not sqlite_db )
throw std :: runtime_error ( std :: format ( "{}: Database not open! Please contact a developer." , __PRETTY_FUNCTION__ ));
if ( sqlite3_exec ( sqlite_db , std :: format ( "DELETE FROM recipe_ingredient WHERE recipe_id={} AND ingredient_id={};" , recipe_id , ingredient_id ). c_str (),
nullptr , nullptr , nullptr ) not_eq SQLITE_OK ) {
throw std :: runtime_error ( std :: format ( "Failed to disconnect recipe with ID {} from ingredient with ID {}." , recipe_id , ingredient_id ));
}
}
2024-10-21 14:05:26 +02:00
void db :: conn_recipe_tag ( const int recipe_id , const int tag_id ) {
2024-10-11 17:43:28 +02:00
if ( not sqlite_db )
throw std :: runtime_error ( std :: format ( "{}: Database not open! Please contact a developer." , __PRETTY_FUNCTION__ ));
2024-09-24 18:15:37 +02:00
2024-10-21 13:54:07 +02:00
if ( sqlite3_exec ( sqlite_db , std :: format ( "INSERT OR IGNORE INTO recipe_tag(recipe_id, tag_id) VALUES({},{});" , recipe_id , tag_id ). c_str (),
2024-10-11 17:43:28 +02:00
nullptr , nullptr , nullptr ) not_eq SQLITE_OK ) {
throw std :: runtime_error ( std :: format ( "Failed to connect recipe with ID {} to tag with ID {}" ,
recipe_id , tag_id ));
}
2024-09-24 18:15:37 +02:00
}
2024-10-21 14:03:04 +02:00
2024-10-21 14:05:26 +02:00
void db :: disconn_recipe_tag ( const int recipe_id , const int tag_id ) {
2024-10-21 14:03:04 +02:00
if ( not sqlite_db )
throw std :: runtime_error ( std :: format ( "{}: Database not open! Please contact a developer." , __PRETTY_FUNCTION__ ));
if ( sqlite3_exec ( sqlite_db , std :: format ( "DELETE FROM recipe_tag WHERE recipe_id={} AND tag_id={};" , recipe_id , tag_id ). c_str (),
nullptr , nullptr , nullptr ) not_eq SQLITE_OK ) {
throw std :: runtime_error ( std :: format ( "Failed to disconnect recipe with ID {} from tag with ID {}." , recipe_id , tag_id ));
}
}