Files
farmcal/cmd/server/db.go

353 lines
13 KiB
Go

package main
import (
"database/sql"
"fmt"
)
func ensureSchema(db *sql.DB) error {
stmts := []string{
`CREATE TABLE IF NOT EXISTS settings(
id TINYINT PRIMARY KEY,
days_per_month INT NOT NULL DEFAULT 2,
current_month TINYINT NOT NULL DEFAULT 1,
current_cycle INT NOT NULL DEFAULT 0,
current_day TINYINT NOT NULL DEFAULT 1
)`,
`ALTER TABLE settings ADD COLUMN IF NOT EXISTS current_month TINYINT NOT NULL DEFAULT 1`,
`ALTER TABLE settings ADD COLUMN IF NOT EXISTS current_cycle INT NOT NULL DEFAULT 0`,
`ALTER TABLE settings ADD COLUMN IF NOT EXISTS current_day TINYINT NOT NULL DEFAULT 1`,
`INSERT INTO settings(id,days_per_month,current_month,current_cycle,current_day) VALUES (1,2,1,0,1) ON DUPLICATE KEY UPDATE id=id`,
`UPDATE settings SET current_month=1 WHERE current_month < 1 OR current_month > 12`,
`UPDATE settings SET current_cycle=0 WHERE current_cycle < 0`,
`UPDATE settings SET current_day=1 WHERE current_day < 1`,
`CREATE TABLE IF NOT EXISTS fields(
id BIGINT AUTO_INCREMENT PRIMARY KEY,
number INT NOT NULL UNIQUE,
name VARCHAR(120) NOT NULL DEFAULT '',
owned TINYINT(1) NOT NULL DEFAULT 0,
group_key VARCHAR(64) NOT NULL DEFAULT '',
group_name VARCHAR(120) NOT NULL DEFAULT '',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
)`,
`ALTER TABLE fields ADD COLUMN IF NOT EXISTS owned TINYINT(1) NOT NULL DEFAULT 0`,
`ALTER TABLE fields ADD COLUMN IF NOT EXISTS group_key VARCHAR(64) NOT NULL DEFAULT ''`,
`ALTER TABLE fields ADD COLUMN IF NOT EXISTS group_name VARCHAR(120) NOT NULL DEFAULT ''`,
`CREATE TABLE IF NOT EXISTS crops(
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(80) NOT NULL UNIQUE,
sow_start_month TINYINT NOT NULL,
sow_end_month TINYINT NOT NULL,
grow_months TINYINT NOT NULL,
best_sale_month TINYINT NOT NULL DEFAULT 0,
regrow_enabled TINYINT(1) NOT NULL DEFAULT 0,
regrow_cycles INT NOT NULL DEFAULT 0
)`,
`ALTER TABLE crops ADD COLUMN IF NOT EXISTS best_sale_month TINYINT NOT NULL DEFAULT 0`,
`ALTER TABLE crops ADD COLUMN IF NOT EXISTS regrow_enabled TINYINT(1) NOT NULL DEFAULT 0`,
`ALTER TABLE crops ADD COLUMN IF NOT EXISTS regrow_cycles INT NOT NULL DEFAULT 0`,
`UPDATE crops SET regrow_cycles=0 WHERE regrow_cycles < 0`,
`UPDATE crops SET best_sale_month=0 WHERE best_sale_month < 0 OR best_sale_month > 12`,
`CREATE TABLE IF NOT EXISTS products(
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(120) NOT NULL UNIQUE,
best_sale_month TINYINT NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
)`,
`CREATE TABLE IF NOT EXISTS crop_steps(
id BIGINT AUTO_INCREMENT PRIMARY KEY,
crop_id BIGINT NOT NULL,
phase ENUM('pre','post') NOT NULL,
month_offset INT NOT NULL DEFAULT 0,
title VARCHAR(140) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_crop_steps_crop FOREIGN KEY(crop_id) REFERENCES crops(id) ON DELETE CASCADE
)`,
`CREATE TABLE IF NOT EXISTS custom_task_templates(
id BIGINT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(140) NOT NULL UNIQUE,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
)`,
`CREATE TABLE IF NOT EXISTS custom_tasks(
id BIGINT AUTO_INCREMENT PRIMARY KEY,
template_id BIGINT NULL,
title VARCHAR(140) NOT NULL,
task_period INT NOT NULL DEFAULT 0,
month TINYINT NOT NULL,
day TINYINT NOT NULL,
year_offset SMALLINT NOT NULL DEFAULT 0,
target_name VARCHAR(120) NOT NULL DEFAULT '',
notes VARCHAR(255) NOT NULL DEFAULT '',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_custom_tasks_template FOREIGN KEY(template_id) REFERENCES custom_task_templates(id) ON DELETE SET NULL
)`,
`CREATE TABLE IF NOT EXISTS task_completions(
task_uid VARCHAR(190) NOT NULL,
month TINYINT NOT NULL,
day TINYINT NOT NULL,
year_offset SMALLINT NOT NULL DEFAULT 0,
done TINYINT(1) NOT NULL DEFAULT 1,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY(task_uid,month,day,year_offset)
)`,
`CREATE TABLE IF NOT EXISTS plans(
id BIGINT AUTO_INCREMENT PRIMARY KEY,
field_id BIGINT NULL,
target_ref VARCHAR(80) NOT NULL DEFAULT '',
target_name VARCHAR(140) NOT NULL DEFAULT '',
crop_id BIGINT NOT NULL,
start_period INT NOT NULL DEFAULT 0,
start_month TINYINT NOT NULL,
start_day TINYINT NOT NULL,
harvest_period INT NOT NULL DEFAULT 0,
harvest_month TINYINT NOT NULL,
harvest_day TINYINT NOT NULL,
notes VARCHAR(255) NOT NULL DEFAULT '',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_plans_fields FOREIGN KEY(field_id) REFERENCES fields(id) ON DELETE CASCADE,
CONSTRAINT fk_plans_crops FOREIGN KEY(crop_id) REFERENCES crops(id) ON DELETE RESTRICT
)`,
`ALTER TABLE plans MODIFY COLUMN field_id BIGINT NULL`,
`ALTER TABLE plans ADD COLUMN IF NOT EXISTS target_ref VARCHAR(80) NOT NULL DEFAULT '' AFTER field_id`,
`ALTER TABLE plans ADD COLUMN IF NOT EXISTS target_name VARCHAR(140) NOT NULL DEFAULT '' AFTER target_ref`,
`ALTER TABLE plans ADD COLUMN IF NOT EXISTS start_period INT NOT NULL DEFAULT 0 AFTER crop_id`,
`ALTER TABLE plans ADD COLUMN IF NOT EXISTS harvest_period INT NOT NULL DEFAULT 0 AFTER start_day`,
`UPDATE plans SET start_period = start_month - 1 WHERE start_period = 0 AND start_month <> 1`,
`UPDATE plans SET harvest_period = start_period + (CASE WHEN harvest_month >= start_month THEN harvest_month - start_month ELSE harvest_month + 12 - start_month END) WHERE harvest_period = 0`,
`ALTER TABLE custom_tasks ADD COLUMN IF NOT EXISTS task_period INT NOT NULL DEFAULT 0 AFTER title`,
`UPDATE custom_tasks SET task_period = (year_offset * 12) + (month - 1) WHERE task_period = 0 AND (year_offset <> 0 OR month <> 1)`,
}
for _, stmt := range stmts {
if _, err := db.Exec(stmt); err != nil {
return err
}
}
return seedCrops(db)
}
func (a *App) getSettings() (Settings, error) {
var s Settings
err := a.db.QueryRow(`SELECT days_per_month,current_month,current_cycle,current_day FROM settings WHERE id=1`).
Scan(&s.DaysPerMonth, &s.CurrentMonth, &s.CurrentCycle, &s.CurrentDay)
return s, err
}
func (a *App) listFields() ([]Field, error) {
rows, err := a.db.Query(`SELECT id,number,name,owned,COALESCE(group_key,''),COALESCE(group_name,'') FROM fields ORDER BY number`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []Field
for rows.Next() {
var f Field
if err := rows.Scan(&f.ID, &f.Number, &f.Name, &f.Owned, &f.GroupKey, &f.GroupName); err != nil {
return nil, err
}
out = append(out, f)
}
return out, rows.Err()
}
func (a *App) getFieldsByIDs(ids []int64) ([]Field, error) {
args := make([]any, 0, len(ids))
for _, id := range ids {
args = append(args, id)
}
q := fmt.Sprintf(`SELECT id,number,name,owned,COALESCE(group_key,''),COALESCE(group_name,'') FROM fields WHERE id IN (%s)`, placeholders(len(ids)))
rows, err := a.db.Query(q, args...)
if err != nil {
return nil, err
}
defer rows.Close()
var out []Field
for rows.Next() {
var f Field
if err := rows.Scan(&f.ID, &f.Number, &f.Name, &f.Owned, &f.GroupKey, &f.GroupName); err != nil {
return nil, err
}
out = append(out, f)
}
return out, rows.Err()
}
func (a *App) listCrops() ([]Crop, error) {
rows, err := a.db.Query(`SELECT id,name,sow_start_month,sow_end_month,grow_months,best_sale_month,regrow_enabled,regrow_cycles FROM crops ORDER BY name`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []Crop
for rows.Next() {
var c Crop
if err := rows.Scan(&c.ID, &c.Name, &c.SowStartMonth, &c.SowEndMonth, &c.GrowMonths, &c.BestSaleMonth, &c.RegrowEnabled, &c.RegrowCycles); err != nil {
return nil, err
}
out = append(out, c)
}
return out, rows.Err()
}
func (a *App) listProducts() ([]Product, error) {
rows, err := a.db.Query(`SELECT id,name,best_sale_month FROM products ORDER BY name`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []Product
for rows.Next() {
var p Product
if err := rows.Scan(&p.ID, &p.Name, &p.BestSaleMonth); err != nil {
return nil, err
}
out = append(out, p)
}
return out, rows.Err()
}
func (a *App) listPlans() ([]Plan, error) {
rows, err := a.db.Query(`
SELECT p.id,p.field_id,COALESCE(p.target_ref,''),COALESCE(p.target_name,''),p.crop_id,COALESCE(c.name,''),COALESCE(c.grow_months,1),p.start_period,p.start_month,p.start_day,p.harvest_period,p.harvest_month,p.harvest_day,COALESCE(p.notes,''),COALESCE(c.regrow_enabled,0),COALESCE(c.regrow_cycles,0)
FROM plans p
JOIN crops c ON c.id=p.crop_id
ORDER BY p.start_period,p.start_day,p.id DESC`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []Plan
for rows.Next() {
var p Plan
if err := rows.Scan(&p.ID, &p.FieldID, &p.TargetRef, &p.TargetName, &p.CropID, &p.CropName, &p.GrowMonths, &p.StartPeriod, &p.StartMonth, &p.StartDay, &p.HarvestPeriod, &p.HarvestMonth, &p.HarvestDay, &p.Notes, &p.RegrowEnabled, &p.RegrowCycles); err != nil {
return nil, err
}
p.StartCycle = periodCycle(p.StartPeriod)
p.HarvestCycle = periodCycle(p.HarvestPeriod)
out = append(out, p)
}
return out, rows.Err()
}
func (a *App) listCropSteps() ([]CropStep, error) {
rows, err := a.db.Query(`
SELECT s.id,s.crop_id,COALESCE(c.name,''),s.phase,s.month_offset,s.title
FROM crop_steps s
JOIN crops c ON c.id=s.crop_id
ORDER BY c.name, s.phase, s.month_offset, s.id`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []CropStep
for rows.Next() {
var s CropStep
if err := rows.Scan(&s.ID, &s.CropID, &s.CropName, &s.Phase, &s.MonthOffset, &s.Title); err != nil {
return nil, err
}
out = append(out, s)
}
return out, rows.Err()
}
func (a *App) listCropStepsMap() (map[int64][]CropStep, error) {
all, err := a.listCropSteps()
if err != nil {
return nil, err
}
m := make(map[int64][]CropStep)
for _, s := range all {
m[s.CropID] = append(m[s.CropID], s)
}
return m, nil
}
func (a *App) listCustomTaskTemplates() ([]CustomTaskTemplate, error) {
rows, err := a.db.Query(`SELECT id,title FROM custom_task_templates ORDER BY title`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []CustomTaskTemplate
for rows.Next() {
var t CustomTaskTemplate
if err := rows.Scan(&t.ID, &t.Title); err != nil {
return nil, err
}
out = append(out, t)
}
return out, rows.Err()
}
func (a *App) listCustomTasks() ([]CustomTask, error) {
rows, err := a.db.Query(`
SELECT id,template_id,title,task_period,month,day,year_offset,COALESCE(target_name,''),COALESCE(notes,'')
FROM custom_tasks
ORDER BY task_period, day, title`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []CustomTask
for rows.Next() {
var t CustomTask
if err := rows.Scan(&t.ID, &t.TemplateID, &t.Title, &t.TaskPeriod, &t.Month, &t.Day, &t.YearOffset, &t.TargetName, &t.Notes); err != nil {
return nil, err
}
t.Month = periodMonth(t.TaskPeriod)
t.Cycle = periodCycle(t.TaskPeriod)
out = append(out, t)
}
return out, rows.Err()
}
func (a *App) listTaskCompletionsMap() (map[string]bool, error) {
rows, err := a.db.Query(`SELECT task_uid,month,day,year_offset,done FROM task_completions WHERE done=1`)
if err != nil {
return nil, err
}
defer rows.Close()
out := make(map[string]bool)
for rows.Next() {
var uid string
var month, day, yearOffset int
var done bool
if err := rows.Scan(&uid, &month, &day, &yearOffset, &done); err != nil {
return nil, err
}
if done {
out[completionKey(uid, month, day, yearOffset)] = true
}
}
return out, rows.Err()
}
func seedCrops(db *sql.DB) error {
items := []Crop{
{Name: "Weizen", SowStartMonth: 9, SowEndMonth: 11, GrowMonths: 10},
{Name: "Gerste", SowStartMonth: 9, SowEndMonth: 10, GrowMonths: 9},
{Name: "Hafer", SowStartMonth: 3, SowEndMonth: 4, GrowMonths: 4},
{Name: "Raps", SowStartMonth: 8, SowEndMonth: 9, GrowMonths: 11},
{Name: "Mais", SowStartMonth: 4, SowEndMonth: 5, GrowMonths: 6},
{Name: "Sorghum", SowStartMonth: 4, SowEndMonth: 5, GrowMonths: 5},
{Name: "Sojabohnen", SowStartMonth: 4, SowEndMonth: 5, GrowMonths: 5},
{Name: "Sonnenblumen", SowStartMonth: 4, SowEndMonth: 5, GrowMonths: 5},
{Name: "Kartoffeln", SowStartMonth: 3, SowEndMonth: 4, GrowMonths: 7},
{Name: "Zuckerrueben", SowStartMonth: 3, SowEndMonth: 4, GrowMonths: 8},
{Name: "Baumwolle", SowStartMonth: 2, SowEndMonth: 3, GrowMonths: 8},
}
for _, c := range items {
if _, err := db.Exec(`INSERT INTO crops(name,sow_start_month,sow_end_month,grow_months,best_sale_month,regrow_enabled,regrow_cycles) VALUES (?,?,?,?,0,0,0) ON DUPLICATE KEY UPDATE sow_start_month=VALUES(sow_start_month),sow_end_month=VALUES(sow_end_month),grow_months=VALUES(grow_months)`, c.Name, c.SowStartMonth, c.SowEndMonth, c.GrowMonths); err != nil {
return err
}
}
return nil
}