Files
beads/internal/storage/dolt/dirty.go
mayor 1dc36098a3 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>
2026-01-14 21:06:10 -08:00

109 lines
3.2 KiB
Go

package dolt
import (
"context"
"fmt"
"strings"
"time"
)
// GetDirtyIssues returns IDs of issues that have been modified since last export
func (s *DoltStore) GetDirtyIssues(ctx context.Context) ([]string, error) {
rows, err := s.db.QueryContext(ctx, `
SELECT issue_id FROM dirty_issues ORDER BY marked_at ASC
`)
if err != nil {
return nil, fmt.Errorf("failed to get dirty issues: %w", err)
}
defer rows.Close()
var ids []string
for rows.Next() {
var id string
if err := rows.Scan(&id); err != nil {
return nil, fmt.Errorf("failed to scan issue id: %w", err)
}
ids = append(ids, id)
}
return ids, rows.Err()
}
// GetDirtyIssueHash returns the dirty hash for a specific issue
func (s *DoltStore) GetDirtyIssueHash(ctx context.Context, issueID string) (string, error) {
var hash string
err := s.db.QueryRowContext(ctx, `
SELECT i.content_hash FROM issues i
JOIN dirty_issues d ON i.id = d.issue_id
WHERE d.issue_id = ?
`, issueID).Scan(&hash)
if err != nil {
return "", fmt.Errorf("failed to get dirty issue hash: %w", err)
}
return hash, nil
}
// ClearDirtyIssuesByID removes specific issues from the dirty list
func (s *DoltStore) ClearDirtyIssuesByID(ctx context.Context, issueIDs []string) error {
if len(issueIDs) == 0 {
return nil
}
placeholders := make([]string, len(issueIDs))
args := make([]interface{}, len(issueIDs))
for i, id := range issueIDs {
placeholders[i] = "?"
args[i] = id
}
query := fmt.Sprintf("DELETE FROM dirty_issues WHERE issue_id IN (%s)", strings.Join(placeholders, ","))
_, err := s.db.ExecContext(ctx, query, args...)
if err != nil {
return fmt.Errorf("failed to clear dirty issues: %w", err)
}
return nil
}
// GetExportHash returns the last export hash for an issue
func (s *DoltStore) GetExportHash(ctx context.Context, issueID string) (string, error) {
var hash string
err := s.db.QueryRowContext(ctx, `
SELECT content_hash FROM export_hashes WHERE issue_id = ?
`, issueID).Scan(&hash)
if err != nil {
return "", nil // Not found is OK
}
return hash, nil
}
// SetExportHash stores the export hash for an issue
func (s *DoltStore) SetExportHash(ctx context.Context, issueID, contentHash string) error {
_, err := s.db.ExecContext(ctx, `
INSERT INTO export_hashes (issue_id, content_hash, exported_at)
VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE content_hash = VALUES(content_hash), exported_at = VALUES(exported_at)
`, issueID, contentHash, time.Now())
if err != nil {
return fmt.Errorf("failed to set export hash: %w", err)
}
return nil
}
// ClearAllExportHashes removes all export hashes (for full re-export)
func (s *DoltStore) ClearAllExportHashes(ctx context.Context) error {
_, err := s.db.ExecContext(ctx, "DELETE FROM export_hashes")
if err != nil {
return fmt.Errorf("failed to clear export hashes: %w", err)
}
return nil
}
// GetJSONLFileHash returns the stored JSONL file hash
func (s *DoltStore) GetJSONLFileHash(ctx context.Context) (string, error) {
return s.GetMetadata(ctx, "jsonl_file_hash")
}
// SetJSONLFileHash stores the JSONL file hash
func (s *DoltStore) SetJSONLFileHash(ctx context.Context, fileHash string) error {
return s.SetMetadata(ctx, "jsonl_file_hash", fileHash)
}