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:
110
internal/storage/dolt/config.go
Normal file
110
internal/storage/dolt/config.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package dolt
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SetConfig sets a configuration value
|
||||
func (s *DoltStore) SetConfig(ctx context.Context, key, value string) error {
|
||||
_, err := s.db.ExecContext(ctx, `
|
||||
INSERT INTO config (` + "`key`" + `, value) VALUES (?, ?)
|
||||
ON DUPLICATE KEY UPDATE value = VALUES(value)
|
||||
`, key, value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set config %s: %w", key, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetConfig retrieves a configuration value
|
||||
func (s *DoltStore) GetConfig(ctx context.Context, key string) (string, error) {
|
||||
var value string
|
||||
err := s.db.QueryRowContext(ctx, "SELECT value FROM config WHERE `key` = ?", key).Scan(&value)
|
||||
if err == sql.ErrNoRows {
|
||||
return "", nil
|
||||
}
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get config %s: %w", key, err)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// GetAllConfig retrieves all configuration values
|
||||
func (s *DoltStore) GetAllConfig(ctx context.Context) (map[string]string, error) {
|
||||
rows, err := s.db.QueryContext(ctx, "SELECT `key`, value FROM config")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get all config: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
config := make(map[string]string)
|
||||
for rows.Next() {
|
||||
var key, value string
|
||||
if err := rows.Scan(&key, &value); err != nil {
|
||||
return nil, fmt.Errorf("failed to scan config: %w", err)
|
||||
}
|
||||
config[key] = value
|
||||
}
|
||||
return config, rows.Err()
|
||||
}
|
||||
|
||||
// DeleteConfig removes a configuration value
|
||||
func (s *DoltStore) DeleteConfig(ctx context.Context, key string) error {
|
||||
_, err := s.db.ExecContext(ctx, "DELETE FROM config WHERE `key` = ?", key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete config %s: %w", key, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetMetadata sets a metadata value
|
||||
func (s *DoltStore) SetMetadata(ctx context.Context, key, value string) error {
|
||||
_, err := s.db.ExecContext(ctx, `
|
||||
INSERT INTO metadata (` + "`key`" + `, value) VALUES (?, ?)
|
||||
ON DUPLICATE KEY UPDATE value = VALUES(value)
|
||||
`, key, value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set metadata %s: %w", key, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetMetadata retrieves a metadata value
|
||||
func (s *DoltStore) GetMetadata(ctx context.Context, key string) (string, error) {
|
||||
var value string
|
||||
err := s.db.QueryRowContext(ctx, "SELECT value FROM metadata WHERE `key` = ?", key).Scan(&value)
|
||||
if err == sql.ErrNoRows {
|
||||
return "", nil
|
||||
}
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get metadata %s: %w", key, err)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// GetCustomStatuses returns custom status values from config
|
||||
func (s *DoltStore) GetCustomStatuses(ctx context.Context) ([]string, error) {
|
||||
value, err := s.GetConfig(ctx, "status.custom")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if value == "" {
|
||||
return nil, nil
|
||||
}
|
||||
return strings.Split(value, ","), nil
|
||||
}
|
||||
|
||||
// GetCustomTypes returns custom issue type values from config
|
||||
func (s *DoltStore) GetCustomTypes(ctx context.Context) ([]string, error) {
|
||||
value, err := s.GetConfig(ctx, "types.custom")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if value == "" {
|
||||
return nil, nil
|
||||
}
|
||||
return strings.Split(value, ","), nil
|
||||
}
|
||||
Reference in New Issue
Block a user