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

@@ -2,6 +2,7 @@ package doctor
import (
"bufio"
"context"
"database/sql"
"encoding/json"
"fmt"
@@ -16,6 +17,7 @@ import (
"github.com/steveyegge/beads/cmd/bd/doctor/fix"
"github.com/steveyegge/beads/internal/beads"
"github.com/steveyegge/beads/internal/configfile"
storagefactory "github.com/steveyegge/beads/internal/storage/factory"
"gopkg.in/yaml.v3"
)
@@ -27,8 +29,85 @@ type localConfig struct {
// CheckDatabaseVersion checks the database version and migration status
func CheckDatabaseVersion(path string, cliVersion string) DoctorCheck {
// Follow redirect to resolve actual beads directory
beadsDir := resolveBeadsDir(filepath.Join(path, ".beads"))
backend, beadsDir := getBackendAndBeadsDir(path)
// Dolt backend: directory-backed store; version lives in metadata table.
if backend == configfile.BackendDolt {
doltPath := filepath.Join(beadsDir, "dolt")
if _, err := os.Stat(doltPath); os.IsNotExist(err) {
// If JSONL exists, treat as fresh clone / needs init.
issuesJSONL := filepath.Join(beadsDir, "issues.jsonl")
beadsJSONL := filepath.Join(beadsDir, "beads.jsonl")
_, issuesErr := os.Stat(issuesJSONL)
_, beadsErr := os.Stat(beadsJSONL)
if issuesErr == nil || beadsErr == nil {
return DoctorCheck{
Name: "Database",
Status: StatusWarning,
Message: "Fresh clone detected (no dolt database)",
Detail: "Storage: Dolt",
Fix: "Run 'bd init --backend dolt' to create and hydrate the dolt database",
}
}
return DoctorCheck{
Name: "Database",
Status: StatusError,
Message: "No dolt database found",
Detail: "Storage: Dolt",
Fix: "Run 'bd init --backend dolt' to create database",
}
}
ctx := context.Background()
store, err := storagefactory.NewFromConfigWithOptions(ctx, beadsDir, storagefactory.Options{ReadOnly: true})
if err != nil {
return DoctorCheck{
Name: "Database",
Status: StatusError,
Message: "Unable to open database",
Detail: fmt.Sprintf("Storage: Dolt\n\nError: %v", err),
Fix: "Run 'bd init --backend dolt' (or remove and re-init .beads/dolt if corrupted)",
}
}
defer func() { _ = store.Close() }()
dbVersion, err := store.GetMetadata(ctx, "bd_version")
if err != nil {
return DoctorCheck{
Name: "Database",
Status: StatusError,
Message: "Unable to read database version",
Detail: fmt.Sprintf("Storage: Dolt\n\nError: %v", err),
Fix: "Database may be corrupted. Try re-initializing the dolt database with 'bd init --backend dolt'",
}
}
if dbVersion == "" {
return DoctorCheck{
Name: "Database",
Status: StatusWarning,
Message: "Database missing version metadata",
Detail: "Storage: Dolt",
Fix: "Run 'bd migrate' or re-run 'bd init --backend dolt' to set version metadata",
}
}
if dbVersion != cliVersion {
return DoctorCheck{
Name: "Database",
Status: StatusWarning,
Message: fmt.Sprintf("version %s (CLI: %s)", dbVersion, cliVersion),
Detail: "Storage: Dolt",
Fix: "Update bd CLI and re-run (dolt metadata will be updated automatically by the daemon)",
}
}
return DoctorCheck{
Name: "Database",
Status: StatusOK,
Message: fmt.Sprintf("version %s", dbVersion),
Detail: "Storage: Dolt",
}
}
// Check metadata.json first for custom database name
var dbPath string
@@ -137,8 +216,48 @@ func CheckDatabaseVersion(path string, cliVersion string) DoctorCheck {
// CheckSchemaCompatibility checks if all required tables and columns are present
func CheckSchemaCompatibility(path string) DoctorCheck {
// Follow redirect to resolve actual beads directory
beadsDir := resolveBeadsDir(filepath.Join(path, ".beads"))
backend, beadsDir := getBackendAndBeadsDir(path)
// Dolt backend: no SQLite schema probe. Instead, run a lightweight query sanity check.
if backend == configfile.BackendDolt {
if info, err := os.Stat(filepath.Join(beadsDir, "dolt")); err != nil || !info.IsDir() {
return DoctorCheck{
Name: "Schema Compatibility",
Status: StatusOK,
Message: "N/A (no database)",
}
}
ctx := context.Background()
store, err := storagefactory.NewFromConfigWithOptions(ctx, beadsDir, storagefactory.Options{ReadOnly: true})
if err != nil {
return DoctorCheck{
Name: "Schema Compatibility",
Status: StatusError,
Message: "Failed to open database",
Detail: fmt.Sprintf("Storage: Dolt\n\nError: %v", err),
}
}
defer func() { _ = store.Close() }()
// Exercise core tables/views.
if _, err := store.GetStatistics(ctx); err != nil {
return DoctorCheck{
Name: "Schema Compatibility",
Status: StatusError,
Message: "Database schema is incomplete or incompatible",
Detail: fmt.Sprintf("Storage: Dolt\n\nError: %v", err),
Fix: "Re-run 'bd init --backend dolt' or remove and re-initialize .beads/dolt if corrupted",
}
}
return DoctorCheck{
Name: "Schema Compatibility",
Status: StatusOK,
Message: "Basic queries succeeded",
Detail: "Storage: Dolt",
}
}
// Check metadata.json first for custom database name
var dbPath string
@@ -227,8 +346,57 @@ func CheckSchemaCompatibility(path string) DoctorCheck {
// CheckDatabaseIntegrity runs SQLite's PRAGMA integrity_check
func CheckDatabaseIntegrity(path string) DoctorCheck {
// Follow redirect to resolve actual beads directory
beadsDir := resolveBeadsDir(filepath.Join(path, ".beads"))
backend, beadsDir := getBackendAndBeadsDir(path)
// Dolt backend: SQLite PRAGMA integrity_check doesn't apply.
// We do a lightweight read-only sanity check instead.
if backend == configfile.BackendDolt {
if info, err := os.Stat(filepath.Join(beadsDir, "dolt")); err != nil || !info.IsDir() {
return DoctorCheck{
Name: "Database Integrity",
Status: StatusOK,
Message: "N/A (no database)",
}
}
ctx := context.Background()
store, err := storagefactory.NewFromConfigWithOptions(ctx, beadsDir, storagefactory.Options{ReadOnly: true})
if err != nil {
return DoctorCheck{
Name: "Database Integrity",
Status: StatusError,
Message: "Failed to open database",
Detail: fmt.Sprintf("Storage: Dolt\n\nError: %v", err),
Fix: "Re-run 'bd init --backend dolt' or remove and re-initialize .beads/dolt if corrupted",
}
}
defer func() { _ = store.Close() }()
// Minimal checks: metadata + statistics. If these work, the store is at least readable.
if _, err := store.GetMetadata(ctx, "bd_version"); err != nil {
return DoctorCheck{
Name: "Database Integrity",
Status: StatusError,
Message: "Basic query failed",
Detail: fmt.Sprintf("Storage: Dolt\n\nError: %v", err),
}
}
if _, err := store.GetStatistics(ctx); err != nil {
return DoctorCheck{
Name: "Database Integrity",
Status: StatusError,
Message: "Basic query failed",
Detail: fmt.Sprintf("Storage: Dolt\n\nError: %v", err),
}
}
return DoctorCheck{
Name: "Database Integrity",
Status: StatusOK,
Message: "Basic query check passed",
Detail: "Storage: Dolt (no SQLite integrity_check equivalent)",
}
}
// Get database path (same logic as CheckSchemaCompatibility)
var dbPath string
@@ -340,8 +508,46 @@ func CheckDatabaseIntegrity(path string) DoctorCheck {
// CheckDatabaseJSONLSync checks if database and JSONL are in sync
func CheckDatabaseJSONLSync(path string) DoctorCheck {
// Follow redirect to resolve actual beads directory
beadsDir := resolveBeadsDir(filepath.Join(path, ".beads"))
backend, beadsDir := getBackendAndBeadsDir(path)
// Dolt backend: JSONL is a derived compatibility artifact (export-only today).
// The SQLite-style import/export divergence checks don't apply.
if backend == configfile.BackendDolt {
// Find JSONL file (respects metadata.json override when set).
jsonlPath := ""
if cfg, err := configfile.Load(beadsDir); err == nil && cfg != nil {
if cfg.JSONLExport != "" && !isSystemJSONLFilename(cfg.JSONLExport) {
p := cfg.JSONLPath(beadsDir)
if _, err := os.Stat(p); err == nil {
jsonlPath = p
}
}
}
if jsonlPath == "" {
for _, name := range []string{"issues.jsonl", "beads.jsonl"} {
testPath := filepath.Join(beadsDir, name)
if _, err := os.Stat(testPath); err == nil {
jsonlPath = testPath
break
}
}
}
if jsonlPath == "" {
return DoctorCheck{
Name: "DB-JSONL Sync",
Status: StatusOK,
Message: "N/A (no JSONL file)",
}
}
return DoctorCheck{
Name: "DB-JSONL Sync",
Status: StatusOK,
Message: "N/A (dolt backend)",
Detail: "JSONL is derived from Dolt (export-only); import-only sync checks do not apply",
}
}
// Resolve database path (respects metadata.json override).
dbPath := filepath.Join(beadsDir, beads.CanonicalDatabaseName)
@@ -795,8 +1001,7 @@ func isNoDbModeConfigured(beadsDir string) bool {
// irreversible. The user must make an explicit decision to delete their
// closed issue history. We only provide guidance, never action.
func CheckDatabaseSize(path string) DoctorCheck {
// Follow redirect to resolve actual beads directory
beadsDir := resolveBeadsDir(filepath.Join(path, ".beads"))
_, beadsDir := getBackendAndBeadsDir(path)
// Get database path
var dbPath string