package sqlite import ( "context" "fmt" "github.com/steveyegge/beads/internal/types" ) // AddIssueComment adds a comment to an issue func (s *SQLiteStorage) AddIssueComment(ctx context.Context, issueID, author, text string) (*types.Comment, error) { // Verify issue exists var exists bool err := s.db.QueryRowContext(ctx, `SELECT EXISTS(SELECT 1 FROM issues WHERE id = ?)`, issueID).Scan(&exists) if err != nil { return nil, fmt.Errorf("failed to check issue existence: %w", err) } if !exists { return nil, fmt.Errorf("issue %s not found", issueID) } // Insert comment result, err := s.db.ExecContext(ctx, ` INSERT INTO comments (issue_id, author, text, created_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP) `, issueID, author, text) if err != nil { return nil, fmt.Errorf("failed to insert comment: %w", err) } // Get the inserted comment ID commentID, err := result.LastInsertId() if err != nil { return nil, fmt.Errorf("failed to get comment ID: %w", err) } // Fetch the complete comment comment := &types.Comment{} err = s.db.QueryRowContext(ctx, ` SELECT id, issue_id, author, text, created_at FROM comments WHERE id = ? `, commentID).Scan(&comment.ID, &comment.IssueID, &comment.Author, &comment.Text, &comment.CreatedAt) if err != nil { return nil, fmt.Errorf("failed to fetch comment: %w", err) } // Mark issue as dirty for JSONL export if err := s.MarkIssueDirty(ctx, issueID); err != nil { return nil, fmt.Errorf("failed to mark issue dirty: %w", err) } return comment, nil } // GetIssueComments retrieves all comments for an issue func (s *SQLiteStorage) GetIssueComments(ctx context.Context, issueID string) ([]*types.Comment, error) { rows, err := s.db.QueryContext(ctx, ` SELECT id, issue_id, author, text, created_at FROM comments WHERE issue_id = ? ORDER BY created_at ASC `, issueID) if err != nil { return nil, fmt.Errorf("failed to query comments: %w", err) } defer func() { _ = rows.Close() }() var comments []*types.Comment for rows.Next() { comment := &types.Comment{} err := rows.Scan(&comment.ID, &comment.IssueID, &comment.Author, &comment.Text, &comment.CreatedAt) if err != nil { return nil, fmt.Errorf("failed to scan comment: %w", err) } comments = append(comments, comment) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("error iterating comments: %w", err) } return comments, nil } // GetCommentsForIssues fetches comments for multiple issues in a single query // Returns a map of issue_id -> []*Comment func (s *SQLiteStorage) GetCommentsForIssues(ctx context.Context, issueIDs []string) (map[string][]*types.Comment, error) { if len(issueIDs) == 0 { return make(map[string][]*types.Comment), nil } // Build placeholders for IN clause placeholders := make([]interface{}, len(issueIDs)) for i, id := range issueIDs { placeholders[i] = id } query := fmt.Sprintf(` SELECT id, issue_id, author, text, created_at FROM comments WHERE issue_id IN (%s) ORDER BY issue_id, created_at ASC `, buildPlaceholders(len(issueIDs))) // #nosec G201 -- placeholders are generated internally rows, err := s.db.QueryContext(ctx, query, placeholders...) if err != nil { return nil, fmt.Errorf("failed to batch get comments: %w", err) } defer func() { _ = rows.Close() }() result := make(map[string][]*types.Comment) for rows.Next() { comment := &types.Comment{} err := rows.Scan(&comment.ID, &comment.IssueID, &comment.Author, &comment.Text, &comment.CreatedAt) if err != nil { return nil, fmt.Errorf("failed to scan comment: %w", err) } result[comment.IssueID] = append(result[comment.IssueID], comment) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("error iterating comments: %w", err) } return result, nil }