package sqlite import ( "database/sql" "fmt" "time" "github.com/google/uuid" "task/internal/ticket" ) // TicketStore implements ticket.Store on SQLite. type TicketStore struct { db *DB } // NewTicketStore returns a ticket store. func NewTicketStore(db *DB) *TicketStore { return &TicketStore{db: db} } func scanTicket(scanner interface{ Scan(dest ...any) error }) (ticket.Ticket, error) { var t ticket.Ticket var created, updated, synced string var status string err := scanner.Scan( &t.ID, &t.Title, &t.Description, &status, &t.ProjectID, &created, &updated, &t.Source, &t.ExternalID, &t.ExternalURL, &synced, ) if err != nil { return ticket.Ticket{}, err } t.Status = ticket.Status(status) t.CreatedAt = parseTime(created) t.UpdatedAt = parseTime(updated) t.SyncedAt = parseTime(synced) return t, nil } const ticketCols = `id, title, description, status, project_id, created_at, updated_at, source, external_id, external_url, synced_at` // List returns tickets for a project. func (s *TicketStore) List(projectID string) ([]ticket.Ticket, error) { rows, err := s.db.SQL.Query(`SELECT `+ticketCols+` FROM tickets WHERE project_id = ? ORDER BY updated_at DESC`, projectID) if err != nil { return nil, err } defer rows.Close() var out []ticket.Ticket for rows.Next() { t, err := scanTicket(rows) if err != nil { return nil, err } out = append(out, t) } if out == nil { out = []ticket.Ticket{} } return out, rows.Err() } // Get finds a ticket by ID. func (s *TicketStore) Get(id string) (ticket.Ticket, error) { row := s.db.SQL.QueryRow(`SELECT `+ticketCols+` FROM tickets WHERE id = ?`, id) t, err := scanTicket(row) if err == sql.ErrNoRows { return ticket.Ticket{}, fmt.Errorf("ticket %s: not found", id) } if err != nil { return ticket.Ticket{}, err } return t, nil } // Create inserts a ticket. func (s *TicketStore) Create(t ticket.Ticket) (ticket.Ticket, error) { if t.ProjectID == "" { return ticket.Ticket{}, fmt.Errorf("projectId is required") } if t.Title == "" { return ticket.Ticket{}, fmt.Errorf("title is required") } if t.ID == "" { t.ID = uuid.NewString() } if t.Status == "" { t.Status = ticket.StatusTodo } now := time.Now().UTC() if t.CreatedAt.IsZero() { t.CreatedAt = now } if t.UpdatedAt.IsZero() { t.UpdatedAt = now } _, err := s.db.SQL.Exec(` INSERT INTO tickets(`+ticketCols+`) VALUES(?,?,?,?,?,?,?,?,?,?,?)`, t.ID, t.Title, t.Description, string(t.Status), t.ProjectID, formatTime(t.CreatedAt), formatTime(t.UpdatedAt), t.Source, t.ExternalID, t.ExternalURL, formatTime(t.SyncedAt), ) if err != nil { return ticket.Ticket{}, fmt.Errorf("create ticket: %w", err) } return t, nil } // Update replaces a ticket and bumps UpdatedAt. func (s *TicketStore) Update(t ticket.Ticket) error { if t.ID == "" || t.ProjectID == "" { return fmt.Errorf("id and projectId are required") } var created string err := s.db.SQL.QueryRow(`SELECT created_at FROM tickets WHERE id = ? AND project_id = ?`, t.ID, t.ProjectID).Scan(&created) if err == sql.ErrNoRows { return fmt.Errorf("ticket %s: not found", t.ID) } if err != nil { return err } t.CreatedAt = parseTime(created) t.UpdatedAt = time.Now().UTC() res, err := s.db.SQL.Exec(` UPDATE tickets SET title=?, description=?, status=?, project_id=?, created_at=?, updated_at=?, source=?, external_id=?, external_url=?, synced_at=? WHERE id=?`, t.Title, t.Description, string(t.Status), t.ProjectID, formatTime(t.CreatedAt), formatTime(t.UpdatedAt), t.Source, t.ExternalID, t.ExternalURL, formatTime(t.SyncedAt), t.ID, ) if err != nil { return err } n, _ := res.RowsAffected() if n == 0 { return fmt.Errorf("ticket %s: not found", t.ID) } return nil } // MarkSynced sets SyncedAt = UpdatedAt without bumping UpdatedAt. func (s *TicketStore) MarkSynced(id string) error { if id == "" { return fmt.Errorf("ticket id is required") } res, err := s.db.SQL.Exec(`UPDATE tickets SET synced_at = updated_at WHERE id = ?`, id) if err != nil { return err } n, _ := res.RowsAffected() if n == 0 { return fmt.Errorf("ticket %s: not found", id) } return nil } // Move relocates a ticket to another project. func (s *TicketStore) Move(id, toProjectID string) (ticket.Ticket, error) { if id == "" { return ticket.Ticket{}, fmt.Errorf("ticket id is required") } if toProjectID == "" { return ticket.Ticket{}, fmt.Errorf("projectId is required") } t, err := s.Get(id) if err != nil { return ticket.Ticket{}, err } t.ProjectID = toProjectID t.UpdatedAt = time.Now().UTC() _, err = s.db.SQL.Exec(`UPDATE tickets SET project_id=?, updated_at=? WHERE id=?`, toProjectID, formatTime(t.UpdatedAt), id) if err != nil { return ticket.Ticket{}, err } return t, nil } // Delete removes a ticket. func (s *TicketStore) Delete(id string) error { res, err := s.db.SQL.Exec(`DELETE FROM tickets WHERE id = ?`, id) if err != nil { return err } n, _ := res.RowsAffected() if n == 0 { return fmt.Errorf("ticket %s: not found", id) } return nil }