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:
@@ -0,0 +1,108 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user