Files
beads/internal/storage/dolt/config.go
joe f845bb1be3 fix(dolt): add YAML fallback for custom types/statuses
When the Dolt database connection is temporarily unavailable (e.g.,
stale connection, server restart), GetCustomTypes() and GetCustomStatuses()
now fall back to reading from config.yaml instead of failing.

This matches the SQLite storage behavior and fixes the stop hook
failure where `gt costs record` would fail with "invalid issue type: event"
when the Dolt connection was broken.

The fallback is checked in two cases:
1. When database query returns an error
2. When database has no custom types/statuses configured

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 20:25:48 -08:00

158 lines
4.7 KiB
Go

package dolt
import (
"context"
"database/sql"
"fmt"
"strings"
"github.com/steveyegge/beads/internal/config"
)
// 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.
// If the database doesn't have custom statuses configured, falls back to config.yaml.
// Returns an empty slice if no custom statuses are configured.
func (s *DoltStore) GetCustomStatuses(ctx context.Context) ([]string, error) {
value, err := s.GetConfig(ctx, "status.custom")
if err != nil {
// On database error, try fallback to config.yaml
if yamlStatuses := config.GetCustomStatusesFromYAML(); len(yamlStatuses) > 0 {
return yamlStatuses, nil
}
return nil, err
}
if value != "" {
return parseCommaSeparatedList(value), nil
}
// Fallback to config.yaml when database doesn't have status.custom set.
if yamlStatuses := config.GetCustomStatusesFromYAML(); len(yamlStatuses) > 0 {
return yamlStatuses, nil
}
return nil, nil
}
// GetCustomTypes returns custom issue type values from config.
// If the database doesn't have custom types configured, falls back to config.yaml.
// This fallback is essential during operations when the database connection is
// temporarily unavailable or when types.custom hasn't been configured yet.
// Returns an empty slice if no custom types are configured.
func (s *DoltStore) GetCustomTypes(ctx context.Context) ([]string, error) {
value, err := s.GetConfig(ctx, "types.custom")
if err != nil {
// On database error, try fallback to config.yaml
if yamlTypes := config.GetCustomTypesFromYAML(); len(yamlTypes) > 0 {
return yamlTypes, nil
}
return nil, err
}
if value != "" {
return parseCommaSeparatedList(value), nil
}
// Fallback to config.yaml when database doesn't have types.custom set.
// This allows operations to work with custom types defined in config.yaml
// before they're persisted to the database.
if yamlTypes := config.GetCustomTypesFromYAML(); len(yamlTypes) > 0 {
return yamlTypes, nil
}
return nil, nil
}
// parseCommaSeparatedList splits a comma-separated string into a slice of trimmed entries.
// Empty entries are filtered out.
func parseCommaSeparatedList(value string) []string {
if value == "" {
return nil
}
parts := strings.Split(value, ",")
result := make([]string, 0, len(parts))
for _, p := range parts {
trimmed := strings.TrimSpace(p)
if trimmed != "" {
result = append(result, trimmed)
}
}
return result
}