Implements a complete Dolt storage backend that mirrors the SQLite implementation with MySQL-compatible syntax and adds version control capabilities. Key features: - Full Storage interface implementation (~50 methods) - Version control operations: commit, push, pull, branch, merge, checkout - History queries via AS OF and dolt_history_* tables - Cell-level merge instead of line-level JSONL merge - SQL injection protection with input validation Bug fixes applied during implementation: - Added missing quality_score, work_type, source_system to scanIssue - Fixed Status() to properly parse boolean staged column - Added validation to CreateIssues (was missing in batch create) - Made RenameDependencyPrefix transactional - Expanded GetIssueHistory to return more complete data Test coverage: 17 tests covering CRUD, dependencies, labels, search, comments, events, statistics, and SQL injection protection. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
161 lines
4.2 KiB
Go
161 lines
4.2 KiB
Go
package dolt
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/steveyegge/beads/internal/types"
|
|
)
|
|
|
|
// AddComment adds a comment event to an issue
|
|
func (s *DoltStore) AddComment(ctx context.Context, issueID, actor, comment string) error {
|
|
_, err := s.db.ExecContext(ctx, `
|
|
INSERT INTO events (issue_id, event_type, actor, comment)
|
|
VALUES (?, ?, ?, ?)
|
|
`, issueID, types.EventCommented, actor, comment)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to add comment: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetEvents retrieves events for an issue
|
|
func (s *DoltStore) GetEvents(ctx context.Context, issueID string, limit int) ([]*types.Event, error) {
|
|
query := `
|
|
SELECT id, issue_id, event_type, actor, old_value, new_value, comment, created_at
|
|
FROM events
|
|
WHERE issue_id = ?
|
|
ORDER BY created_at DESC
|
|
`
|
|
args := []interface{}{issueID}
|
|
|
|
if limit > 0 {
|
|
query += fmt.Sprintf(" LIMIT %d", limit)
|
|
}
|
|
|
|
rows, err := s.db.QueryContext(ctx, query, args...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get events: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var events []*types.Event
|
|
for rows.Next() {
|
|
var event types.Event
|
|
var oldValue, newValue, comment sql.NullString
|
|
if err := rows.Scan(&event.ID, &event.IssueID, &event.EventType, &event.Actor,
|
|
&oldValue, &newValue, &comment, &event.CreatedAt); err != nil {
|
|
return nil, fmt.Errorf("failed to scan event: %w", err)
|
|
}
|
|
if oldValue.Valid {
|
|
event.OldValue = &oldValue.String
|
|
}
|
|
if newValue.Valid {
|
|
event.NewValue = &newValue.String
|
|
}
|
|
if comment.Valid {
|
|
event.Comment = &comment.String
|
|
}
|
|
events = append(events, &event)
|
|
}
|
|
return events, rows.Err()
|
|
}
|
|
|
|
// AddIssueComment adds a comment to an issue (structured comment)
|
|
func (s *DoltStore) AddIssueComment(ctx context.Context, issueID, author, text string) (*types.Comment, error) {
|
|
result, err := s.db.ExecContext(ctx, `
|
|
INSERT INTO comments (issue_id, author, text, created_at)
|
|
VALUES (?, ?, ?, ?)
|
|
`, issueID, author, text, time.Now())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to add comment: %w", err)
|
|
}
|
|
|
|
id, err := result.LastInsertId()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get comment id: %w", err)
|
|
}
|
|
|
|
return &types.Comment{
|
|
ID: id,
|
|
IssueID: issueID,
|
|
Author: author,
|
|
Text: text,
|
|
CreatedAt: time.Now(),
|
|
}, nil
|
|
}
|
|
|
|
// GetIssueComments retrieves all comments for an issue
|
|
func (s *DoltStore) 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 get comments: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var comments []*types.Comment
|
|
for rows.Next() {
|
|
var c types.Comment
|
|
if err := rows.Scan(&c.ID, &c.IssueID, &c.Author, &c.Text, &c.CreatedAt); err != nil {
|
|
return nil, fmt.Errorf("failed to scan comment: %w", err)
|
|
}
|
|
comments = append(comments, &c)
|
|
}
|
|
return comments, rows.Err()
|
|
}
|
|
|
|
// GetCommentsForIssues retrieves comments for multiple issues
|
|
func (s *DoltStore) GetCommentsForIssues(ctx context.Context, issueIDs []string) (map[string][]*types.Comment, error) {
|
|
if len(issueIDs) == 0 {
|
|
return make(map[string][]*types.Comment), nil
|
|
}
|
|
|
|
placeholders := make([]string, len(issueIDs))
|
|
args := make([]interface{}, len(issueIDs))
|
|
for i, id := range issueIDs {
|
|
placeholders[i] = "?"
|
|
args[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
|
|
`, joinStrings(placeholders, ","))
|
|
|
|
rows, err := s.db.QueryContext(ctx, query, args...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get comments: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
result := make(map[string][]*types.Comment)
|
|
for rows.Next() {
|
|
var c types.Comment
|
|
if err := rows.Scan(&c.ID, &c.IssueID, &c.Author, &c.Text, &c.CreatedAt); err != nil {
|
|
return nil, fmt.Errorf("failed to scan comment: %w", err)
|
|
}
|
|
result[c.IssueID] = append(result[c.IssueID], &c)
|
|
}
|
|
return result, rows.Err()
|
|
}
|
|
|
|
func joinStrings(strs []string, sep string) string {
|
|
if len(strs) == 0 {
|
|
return ""
|
|
}
|
|
result := strs[0]
|
|
for _, s := range strs[1:] {
|
|
result += sep + s
|
|
}
|
|
return result
|
|
}
|