feat(storage): add Dolt backend for version-controlled issue storage
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>
This commit is contained in:
118
internal/storage/dolt/labels.go
Normal file
118
internal/storage/dolt/labels.go
Normal file
@@ -0,0 +1,118 @@
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user