Fix Dolt backend init/daemon/doctor; prevent accidental SQLite artifacts; add integration tests; clean up lint (#1218)

* /{cmd,internal}: get dolt backend init working and allow issue creation

* /{website,internal,docs,cmd}: integration tests and more split backend fixes

* /{cmd,internal}: fix lint issues

* /cmd/bd/doctor/integrity.go: fix unable to query issues bug with dolt backend

* /cmd/bd/daemon.go: remove debug logging
This commit is contained in:
Dustin Brown
2026-01-20 17:34:00 -08:00
committed by GitHub
parent c1ac69da3e
commit d3ccd5cfba
31 changed files with 1071 additions and 305 deletions

View File

@@ -140,7 +140,7 @@ func findJSONLPath(beadsDir string) string {
func acquireBootstrapLock(lockPath string, timeout time.Duration) (*os.File, error) {
// Create lock file
// #nosec G304 - controlled path
f, err := os.OpenFile(lockPath, os.O_CREATE|os.O_RDWR, 0644)
f, err := os.OpenFile(lockPath, os.O_CREATE|os.O_RDWR, 0600)
if err != nil {
return nil, fmt.Errorf("failed to create lock file: %w", err)
}

View File

@@ -439,25 +439,23 @@ func (s *DoltStore) GetStaleIssues(ctx context.Context, filter types.StaleFilter
func (s *DoltStore) GetStatistics(ctx context.Context) (*types.Statistics, error) {
stats := &types.Statistics{}
// Count by status
// Get counts (mirror SQLite semantics: exclude tombstones from TotalIssues, report separately).
// Important: COALESCE to avoid NULL scans when the table is empty.
err := s.db.QueryRowContext(ctx, `
SELECT
COUNT(*) as total,
SUM(CASE WHEN status = 'open' THEN 1 ELSE 0 END) as open_count,
SUM(CASE WHEN status = 'in_progress' THEN 1 ELSE 0 END) as in_progress,
SUM(CASE WHEN status = 'closed' THEN 1 ELSE 0 END) as closed,
SUM(CASE WHEN status = 'blocked' THEN 1 ELSE 0 END) as blocked,
SUM(CASE WHEN status = 'deferred' THEN 1 ELSE 0 END) as deferred,
SUM(CASE WHEN status = 'tombstone' THEN 1 ELSE 0 END) as tombstone,
SUM(CASE WHEN pinned = 1 THEN 1 ELSE 0 END) as pinned
COALESCE(SUM(CASE WHEN status != 'tombstone' THEN 1 ELSE 0 END), 0) as total,
COALESCE(SUM(CASE WHEN status = 'open' THEN 1 ELSE 0 END), 0) as open_count,
COALESCE(SUM(CASE WHEN status = 'in_progress' THEN 1 ELSE 0 END), 0) as in_progress,
COALESCE(SUM(CASE WHEN status = 'closed' THEN 1 ELSE 0 END), 0) as closed,
COALESCE(SUM(CASE WHEN status = 'deferred' THEN 1 ELSE 0 END), 0) as deferred,
COALESCE(SUM(CASE WHEN status = 'tombstone' THEN 1 ELSE 0 END), 0) as tombstone,
COALESCE(SUM(CASE WHEN pinned = 1 THEN 1 ELSE 0 END), 0) as pinned
FROM issues
WHERE status != 'tombstone'
`).Scan(
&stats.TotalIssues,
&stats.OpenIssues,
&stats.InProgressIssues,
&stats.ClosedIssues,
&stats.BlockedIssues,
&stats.DeferredIssues,
&stats.TombstoneIssues,
&stats.PinnedIssues,
@@ -466,6 +464,27 @@ func (s *DoltStore) GetStatistics(ctx context.Context) (*types.Statistics, error
return nil, fmt.Errorf("failed to get statistics: %w", err)
}
// Blocked count (same semantics as SQLite: blocked by open deps).
err = s.db.QueryRowContext(ctx, `
SELECT COUNT(DISTINCT i.id)
FROM issues i
JOIN dependencies d ON i.id = d.issue_id
JOIN issues blocker ON d.depends_on_id = blocker.id
WHERE i.status IN ('open', 'in_progress', 'blocked', 'deferred', 'hooked')
AND d.type = 'blocks'
AND blocker.status IN ('open', 'in_progress', 'blocked', 'deferred', 'hooked')
`).Scan(&stats.BlockedIssues)
if err != nil {
return nil, fmt.Errorf("failed to get blocked count: %w", err)
}
// Ready count (use the ready_issues view).
// Note: view already excludes ephemeral issues and blocked transitive deps.
err = s.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM ready_issues`).Scan(&stats.ReadyIssues)
if err != nil {
return nil, fmt.Errorf("failed to get ready count: %w", err)
}
return stats, nil
}

View File

@@ -4,7 +4,6 @@ package factory
import (
"context"
"fmt"
"path/filepath"
"time"
"github.com/steveyegge/beads/internal/configfile"
@@ -84,9 +83,7 @@ func NewFromConfigWithOptions(ctx context.Context, beadsDir string, opts Options
case configfile.BackendSQLite:
return NewWithOptions(ctx, backend, cfg.DatabasePath(beadsDir), opts)
case configfile.BackendDolt:
// For Dolt, use a subdirectory to store the Dolt database
doltPath := filepath.Join(beadsDir, "dolt")
return NewWithOptions(ctx, backend, doltPath, opts)
return NewWithOptions(ctx, backend, cfg.DatabasePath(beadsDir), opts)
default:
return nil, fmt.Errorf("unknown storage backend in config: %s", backend)
}