package postgres import ( "database/sql" "gardomatic.kleiax.de/internal/storage" ) // TaskModel stores garden tasks in PostgreSQL. // TaskModel implements storage.TaskModelInterface for garden-scoped work items. type TaskModel struct{ DB *sql.DB } const taskColumns = `id, garden_id, plant_id, location_id, template_id, title, description, due_at_start, due_at_end, generated_for, recurrence, recurrence_interval, repeat_from_id, completed_at, completed_by, priority, active, created_by, created_at, updated_at, version, plant_status_on_completion` func scanTask(s scanner) (storage.Task, error) { var task storage.Task err := s.Scan( &task.ID, &task.GardenID, &task.PlantID, &task.LocationID, &task.TemplateID, &task.Title, &task.Description, &task.DueAtStart, &task.DueAtEnd, &task.GeneratedFor, &task.Recurrence, &task.RecurrenceInterval, &task.RepeatFromID, &task.CompletedAt, &task.CompletedBy, &task.Priority, &task.Active, &task.CreatedBy, &task.CreatedAt, &task.UpdatedAt, &task.Version, &task.PlantStatusOnCompletion, ) return task, err } // Insert creates a task; generated task slots remain idempotent under concurrency. func (m TaskModel) Insert(task storage.Task) (storage.Task, error) { if task.RecurrenceInterval < 1 { task.RecurrenceInterval = 1 } ctx, cancel := contextWithTimeout() defer cancel() err := m.DB.QueryRowContext(ctx, ` INSERT INTO tasks ( garden_id, plant_id, location_id, template_id, title, description, due_at_start, due_at_end, generated_for, recurrence, recurrence_interval, repeat_from_id, completed_at, completed_by, priority, active, created_by, plant_status_on_completion) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18) RETURNING id, created_at, updated_at, version`, task.GardenID, task.PlantID, task.LocationID, task.TemplateID, task.Title, task.Description, task.DueAtStart, task.DueAtEnd, task.GeneratedFor, task.Recurrence, task.RecurrenceInterval, task.RepeatFromID, task.CompletedAt, task.CompletedBy, task.Priority, task.Active, task.CreatedBy, task.PlantStatusOnCompletion, ).Scan(&task.ID, &task.CreatedAt, &task.UpdatedAt, &task.Version) if err != nil { return storage.Task{}, recordError(err) } return task, nil } // Get returns a task within its garden. func (m TaskModel) Get(gardenID, id int) (storage.Task, error) { ctx, cancel := contextWithTimeout() defer cancel() task, err := scanTask(m.DB.QueryRowContext(ctx, `SELECT `+taskColumns+` FROM tasks WHERE garden_id = $1 AND id = $2`, gardenID, id)) if err != nil { return storage.Task{}, recordError(err) } return task, nil } // GetAllForGarden lists tasks in a garden. func (m TaskModel) GetAllForGarden(gardenID int) ([]storage.Task, error) { ctx, cancel := contextWithTimeout() defer cancel() rows, err := m.DB.QueryContext(ctx, `SELECT `+taskColumns+` FROM tasks WHERE garden_id = $1 ORDER BY completed_at NULLS FIRST, due_at_end NULLS LAST, priority DESC, id`, gardenID) if err != nil { return nil, err } defer rows.Close() tasks := []storage.Task{} for rows.Next() { task, err := scanTask(rows) if err != nil { return nil, err } tasks = append(tasks, task) } return tasks, rows.Err() } // Update changes a task using optimistic locking. func (m TaskModel) Update(gardenID int, task storage.Task) (storage.Task, error) { if task.RecurrenceInterval < 1 { task.RecurrenceInterval = 1 } ctx, cancel := contextWithTimeout() defer cancel() err := m.DB.QueryRowContext(ctx, ` UPDATE tasks SET plant_id = $1, location_id = $2, template_id = $3, title = $4, description = $5, due_at_start = $6, due_at_end = $7, generated_for = $8, recurrence = $9, recurrence_interval = $10, repeat_from_id = $11, completed_at = $12, completed_by = $13, priority = $14, active = $15, plant_status_on_completion=$16, updated_at = CURRENT_TIMESTAMP, version = version + 1 WHERE garden_id = $17 AND id = $18 AND version = $19 RETURNING updated_at, version`, task.PlantID, task.LocationID, task.TemplateID, task.Title, task.Description, task.DueAtStart, task.DueAtEnd, task.GeneratedFor, task.Recurrence, task.RecurrenceInterval, task.RepeatFromID, task.CompletedAt, task.CompletedBy, task.Priority, task.Active, task.PlantStatusOnCompletion, gardenID, task.ID, task.Version, ).Scan(&task.UpdatedAt, &task.Version) if err != nil { if err == sql.ErrNoRows { return storage.Task{}, storage.ErrEditConflict } return storage.Task{}, err } return task, nil } // Delete removes a task within its garden. func (m TaskModel) Delete(gardenID, id int) error { return deleteByID(m.DB, `DELETE FROM tasks WHERE garden_id = $1 AND id = $2`, gardenID, id) }