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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user