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>
119 lines
3.1 KiB
Go
119 lines
3.1 KiB
Go
package dolt
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/steveyegge/beads/internal/types"
|
|
)
|
|
|
|
// AddLabel adds a label to an issue
|
|
func (s *DoltStore) AddLabel(ctx context.Context, issueID, label, actor string) error {
|
|
_, err := s.db.ExecContext(ctx, `
|
|
INSERT IGNORE INTO labels (issue_id, label) VALUES (?, ?)
|
|
`, issueID, label)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to add label: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// RemoveLabel removes a label from an issue
|
|
func (s *DoltStore) RemoveLabel(ctx context.Context, issueID, label, actor string) error {
|
|
_, err := s.db.ExecContext(ctx, `
|
|
DELETE FROM labels WHERE issue_id = ? AND label = ?
|
|
`, issueID, label)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to remove label: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetLabels retrieves all labels for an issue
|
|
func (s *DoltStore) GetLabels(ctx context.Context, issueID string) ([]string, error) {
|
|
rows, err := s.db.QueryContext(ctx, `
|
|
SELECT label FROM labels WHERE issue_id = ? ORDER BY label
|
|
`, issueID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get labels: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var labels []string
|
|
for rows.Next() {
|
|
var label string
|
|
if err := rows.Scan(&label); err != nil {
|
|
return nil, fmt.Errorf("failed to scan label: %w", err)
|
|
}
|
|
labels = append(labels, label)
|
|
}
|
|
return labels, rows.Err()
|
|
}
|
|
|
|
// GetLabelsForIssues retrieves labels for multiple issues
|
|
func (s *DoltStore) GetLabelsForIssues(ctx context.Context, issueIDs []string) (map[string][]string, error) {
|
|
if len(issueIDs) == 0 {
|
|
return make(map[string][]string), 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 issue_id, label FROM labels
|
|
WHERE issue_id IN (%s)
|
|
ORDER BY issue_id, label
|
|
`, strings.Join(placeholders, ","))
|
|
|
|
rows, err := s.db.QueryContext(ctx, query, args...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get labels for issues: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
result := make(map[string][]string)
|
|
for rows.Next() {
|
|
var issueID, label string
|
|
if err := rows.Scan(&issueID, &label); err != nil {
|
|
return nil, fmt.Errorf("failed to scan label: %w", err)
|
|
}
|
|
result[issueID] = append(result[issueID], label)
|
|
}
|
|
return result, rows.Err()
|
|
}
|
|
|
|
// GetIssuesByLabel retrieves all issues with a specific label
|
|
func (s *DoltStore) GetIssuesByLabel(ctx context.Context, label string) ([]*types.Issue, error) {
|
|
rows, err := s.db.QueryContext(ctx, `
|
|
SELECT i.id FROM issues i
|
|
JOIN labels l ON i.id = l.issue_id
|
|
WHERE l.label = ?
|
|
ORDER BY i.priority ASC, i.created_at DESC
|
|
`, label)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get issues by label: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var issues []*types.Issue
|
|
for rows.Next() {
|
|
var id string
|
|
if err := rows.Scan(&id); err != nil {
|
|
return nil, fmt.Errorf("failed to scan issue id: %w", err)
|
|
}
|
|
issue, err := s.GetIssue(ctx, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if issue != nil {
|
|
issues = append(issues, issue)
|
|
}
|
|
}
|
|
return issues, rows.Err()
|
|
}
|