package main import ( "database/sql" "fmt" "log" ) type ( FoodService struct { db *DB } Food struct { Id int64 `json:"id"` Date string `json:"date"` Food string `json:"food"` Descripton string `json:"description"` Amount float32 `json:"amount"` Per100 float32 `json:"per100"` Energy float32 `json:"energy"` } FoodSearch struct { Food Score int64 `json:"score"` } AggregatedFood struct { Period string `json:"period"` Amount float32 `json:"amount"` AvgPer100 float32 `json:"avgPer100"` Energy float32 `json:"energy"` } ) const foodColumns = "id, date, food, description, amount, per100, energy" const foodAggregatedColumns = "period, amount, avgPer100, energy" func (s *FoodService) GetRecent() (res []Food, err error) { log.Printf("Getting recent food") if s.db == nil || !s.db.Ready { return res, fmt.Errorf("cannot get recent food, db is nil or is not ready") } row, err := s.db.readConn.Query(fmt.Sprintf("SELECT %s from foodView WHERE date > datetime('now', '-%d days') order by date desc;", foodColumns, Settings.FoodDaysLookback)) if err != nil { return res, fmt.Errorf("error getting recent food: %w", err) } for row.Next() { var food Food err = row.Scan(&food.Id, &food.Date, &food.Food, &food.Descripton, &food.Amount, &food.Per100, &food.Energy) if err != nil { log.Printf("error scanning row: %v", err) continue } res = append(res, food) } log.Printf("Got %d recent foods", len(res)) return } func (s *FoodService) GetLastPer100(name string) (res []FoodSearch, err error) { log.Printf("Getting last per100 for %s", name) if s.db == nil || !s.db.Ready { return res, fmt.Errorf("cannot get last per100, db is nil or is not ready") } query := fmt.Sprintf(` with search_results as ( select word, score, f.rowid, f.*, row_number() over (partition by f.food order by score asc, date desc) as rn from foodfix inner join food f on f.food == word where word MATCH ? ) select %s, score from search_results where rn = 1 order by score asc, date desc limit %d `, foodColumns, Settings.SearchLimit) // log.Printf("%#v", query) rows, err := s.db.readConn.Query(query, name) if err != nil { return res, fmt.Errorf("error getting last per100: %w", err) } for rows.Next() { var f FoodSearch err = rows.Scan(&f.Id, &f.Date, &f.Food.Food, &f.Descripton, &f.Amount, &f.Per100, &f.Energy, &f.Score) if err != nil { log.Printf("error scanning row: %v", err) continue } res = append(res, f) } if len(res) == 0 { return nil, fmt.Errorf("no results found for %s", name) } log.Printf("Got %d last per100 foods for %s", len(res), name) return res, nil } func (s *FoodService) Create(food Food) (res Food, err error) { log.Printf("Creating food %v", food) if s.db == nil || !s.db.Ready { return food, fmt.Errorf("cannot create food, db is nil or is not ready") } if food.Food == "" { return food, fmt.Errorf("cannot create food, food is empty") } if food.Amount <= 0 { return food, fmt.Errorf("cannot create food, amount is less than or equal to 0") } var dbres sql.Result if food.Per100 > 0 { dbres, err = s.db.writeConn.Exec("INSERT INTO food (food, description, amount, per100) VALUES (?, ?, ?, ?)", food.Food, food.Descripton, food.Amount, food.Per100) if err != nil { return food, fmt.Errorf("error inserting food: %w", err) } } else { dbres, err = s.db.writeConn.Exec("INSERT INTO food (food, description, amount) VALUES (?, ?, ?)", food.Food, food.Descripton, food.Amount) if err != nil { return food, fmt.Errorf("error inserting food: %w", err) } } id, err := dbres.LastInsertId() if err != nil { return food, fmt.Errorf("error getting last insert id: %w", err) } log.Printf("Created food %s with id %d", food.Food, id) return s.GetById(id) } func (s *FoodService) Update(food Food) (res Food, err error) { log.Printf("Updating food %s with id %d", food.Food, food.Id) if s.db == nil || !s.db.Ready { return food, fmt.Errorf("cannot update food, db is nil or is not ready") } if food.Id <= 0 { return food, fmt.Errorf("cannot update food, id is less than or equal to 0") } if food.Food == "" { return food, fmt.Errorf("cannot update food, food is empty") } if food.Amount <= 0 { return food, fmt.Errorf("cannot update food, amount is less than or equal to 0") } if food.Per100 > 0 { _, err := s.db.writeConn.Exec("UPDATE food SET food = ?, description = ?, amount = ?, per100 = ? WHERE Id = ?", food.Food, food.Descripton, food.Amount, food.Per100, food.Id) if err != nil { return food, fmt.Errorf("error updating food: %w", err) } } else { _, err := s.db.writeConn.Exec("UPDATE food SET food = ?, description = ?, amount = ? WHERE Id = ?", food.Food, food.Descripton, food.Amount, food.Id) if err != nil { return food, fmt.Errorf("error updating food: %w", err) } } log.Printf("Updated food %s with id %d", food.Food, food.Id) return s.GetById(food.Id) } func (s *FoodService) GetById(id int64) (res Food, err error) { log.Printf("Getting food by id %d", id) if s.db == nil || !s.db.Ready { return res, fmt.Errorf("cannot get food by id, db is nil or is not ready") } row := s.db.readConn.QueryRow(fmt.Sprintf("SELECT %s from foodView WHERE id = ?", foodColumns), id) err = row.Scan(&res.Id, &res.Date, &res.Food, &res.Descripton, &res.Amount, &res.Per100, &res.Energy) if err != nil { return res, fmt.Errorf("error scanning row: %w", err) } log.Printf("Got food %s with id %d", res.Food, res.Id) return res, nil } // I could probably refactor this to be less of a disaster... // But I think it'll work for now func (s *FoodService) GetDaily() (res []AggregatedFood, err error) { log.Printf("Getting daily food") if s.db == nil || !s.db.Ready { return res, fmt.Errorf("cannot get daily food, db is nil or is not ready") } row, err := s.db.readConn.Query(fmt.Sprintf("SELECT %s from foodDaily LIMIT %d", foodAggregatedColumns, Settings.FoodDailyLookback)) if err != nil { return res, fmt.Errorf("error getting daily food: %w", err) } for row.Next() { var food AggregatedFood err := row.Scan(&food.Period, &food.Amount, &food.AvgPer100, &food.Energy) if err != nil { log.Printf("error scanning row: %v", err) continue } res = append(res, food) } log.Printf("Got %d daily foods", len(res)) return res, nil } func (s *FoodService) GetWeekly() (res []AggregatedFood, err error) { log.Printf("Getting weekly food") if s.db == nil || !s.db.Ready { return res, fmt.Errorf("cannot get weekly food, db is nil or is not ready") } row, err := s.db.readConn.Query(fmt.Sprintf("SELECT %s from foodWeekly LIMIT %d", foodAggregatedColumns, Settings.FoodWeeklyLookback)) if err != nil { return res, fmt.Errorf("error getting weekly food: %w", err) } for row.Next() { var food AggregatedFood err = row.Scan(&food.Period, &food.Amount, &food.AvgPer100, &food.Energy) if err != nil { log.Printf("error scanning row: %v", err) continue } res = append(res, food) } log.Printf("Got %d weekly foods", len(res)) return res, nil } func (s *FoodService) GetMonthly() (res []AggregatedFood, err error) { log.Printf("Getting monthly food") if s.db == nil || !s.db.Ready { return res, fmt.Errorf("cannot get monthly food, db is nil or is not ready") } row, err := s.db.readConn.Query(fmt.Sprintf("SELECT %s from foodMonthly LIMIT %d", foodAggregatedColumns, Settings.FoodMonthlyLookback)) if err != nil { return res, fmt.Errorf("error getting monthly food: %w", err) } for row.Next() { var food AggregatedFood err = row.Scan(&food.Period, &food.Amount, &food.AvgPer100, &food.Energy) if err != nil { log.Printf("error scanning row: %v", err) continue } res = append(res, food) } log.Printf("Got %d monthly foods", len(res)) return res, nil } func (s *FoodService) GetYearly() (res []AggregatedFood, err error) { log.Printf("Getting yearly food") if s.db == nil || !s.db.Ready { return res, fmt.Errorf("cannot get yearly food, db is nil or is not ready") } row, err := s.db.readConn.Query(fmt.Sprintf("SELECT %s from foodYearly LIMIT %d", foodAggregatedColumns, Settings.FoodYearlyLookback)) if err != nil { return res, fmt.Errorf("error getting yearly food: %w", err) } for row.Next() { var food AggregatedFood err = row.Scan(&food.Period, &food.Amount, &food.AvgPer100, &food.Energy) if err != nil { log.Printf("error scanning row: %v", err) continue } res = append(res, food) } log.Printf("Got %d yearly foods", len(res)) return res, nil }