diff options
Diffstat (limited to 'internal/store/native_tasks.go')
| -rw-r--r-- | internal/store/native_tasks.go | 88 |
1 files changed, 75 insertions, 13 deletions
diff --git a/internal/store/native_tasks.go b/internal/store/native_tasks.go index 218675e..7789a0d 100644 --- a/internal/store/native_tasks.go +++ b/internal/store/native_tasks.go @@ -1,12 +1,21 @@ package store import ( + "database/sql" "encoding/json" + "errors" "time" "task-dashboard/internal/models" ) +// ErrNativeTaskNotFound is returned by CompleteNativeTask, UncompleteNativeTask, +// and RescheduleNativeTask when no row matches the given id -- previously these +// three silently reported success on a 0-row UPDATE (Exec's err is nil even when +// no rows match), so a stale or wrong id from a caller looked identical to a real +// completion: the HTTP response was 200, but nothing in the database changed. +var ErrNativeTaskNotFound = errors.New("native task not found") + // GetNativeTasks returns all non-completed native tasks. func (s *Store) GetNativeTasks() ([]models.Task, error) { rows, err := s.db.Query(` @@ -22,15 +31,37 @@ func (s *Store) GetNativeTasks() ([]models.Task, error) { return scanNativeTasks(rows) } -// GetNativeTasksByDateRange returns non-completed native tasks due within the given range, -// including overdue tasks (due before start) so they keep appearing until completed. +// GetNativeTasksByDateRange returns non-completed native tasks due within the given range. +// Overdue tasks (due before start) are deliberately excluded here -- BuildTimeline fetches +// those separately via GetOverdueNativeTasks so callers that only want "in range" can use this +// without double-counting against that separate fetch. func (s *Store) GetNativeTasksByDateRange(start, end time.Time) ([]models.Task, error) { rows, err := s.db.Query(` SELECT id, content, description, project_name, due_date, priority, completed, labels, created_at FROM native_tasks + WHERE completed = 0 AND due_date IS NOT NULL AND due_date >= ? AND due_date < ? + ORDER BY due_date ASC, priority DESC + `, start, end) + if err != nil { + return nil, err + } + defer func() { _ = rows.Close() }() + return scanNativeTasks(rows) +} + +// GetOverdueNativeTasks returns non-completed native tasks whose due date is +// before the given time. BuildTimeline calls this alongside +// GetNativeTasksByDateRange, whose lower bound excludes anything due before +// the requested range's start -- without this, a task overdue from a +// previous day never gets fetched at all, so it never reaches +// ComputeDaySection to be marked IsOverdue. +func (s *Store) GetOverdueNativeTasks(before time.Time) ([]models.Task, error) { + rows, err := s.db.Query(` + SELECT id, content, description, project_name, due_date, priority, completed, labels, created_at + FROM native_tasks WHERE completed = 0 AND due_date IS NOT NULL AND due_date < ? ORDER BY due_date ASC, priority DESC - `, end) + `, before) if err != nil { return nil, err } @@ -81,31 +112,62 @@ func (s *Store) UpdateNativeTaskDescription(id, description string) error { return err } -// CompleteNativeTask marks a task as completed. +// CompleteNativeTask marks a task as completed. Returns ErrNativeTaskNotFound +// if id doesn't match any row. func (s *Store) CompleteNativeTask(id string) error { - _, err := s.db.Exec(` + result, err := s.db.Exec(` UPDATE native_tasks SET completed = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ? `, id) - return err + if err != nil { + return err + } + return checkRowsAffected(result) } -// RescheduleNativeTask sets a new due date on a task. +// RescheduleNativeTask sets a new due date on a task. Returns +// ErrNativeTaskNotFound if id doesn't match any row. func (s *Store) RescheduleNativeTask(id string, dueDate time.Time) error { - _, err := s.db.Exec(` + result, err := s.db.Exec(` UPDATE native_tasks SET due_date = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? `, dueDate, id) - return err + if err != nil { + return err + } + return checkRowsAffected(result) } -// UncompleteNativeTask marks a task as not completed. +// UncompleteNativeTask marks a task as not completed. Returns +// ErrNativeTaskNotFound if id doesn't match any row. func (s *Store) UncompleteNativeTask(id string) error { - _, err := s.db.Exec(` + result, err := s.db.Exec(` UPDATE native_tasks SET completed = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ? `, id) - return err + if err != nil { + return err + } + return checkRowsAffected(result) +} + +// checkRowsAffected returns ErrNativeTaskNotFound if the update matched no +// rows -- mirrors the RowsAffected() check already used in sqlite.go's +// ApproveAgentSession/DenyAgentSession for the same "silent 0-row update" +// class of bug. +func checkRowsAffected(result sql.Result) error { + affected, err := result.RowsAffected() + if err != nil { + return err + } + if affected == 0 { + return ErrNativeTaskNotFound + } + return nil } -func scanNativeTasks(rows interface{ Next() bool; Scan(...interface{}) error; Err() error }) ([]models.Task, error) { +func scanNativeTasks(rows interface { + Next() bool + Scan(...interface{}) error + Err() error +}) ([]models.Task, error) { var tasks []models.Task for rows.Next() { var t models.Task |
