Implement bd-162: Enforce canonical database name (beads.db)
- Changed bd init to always create beads.db instead of {prefix}.db
- Added migration logic to detect and rename old databases
- Updated findDatabaseInTree to prefer beads.db and warn on multiple .db files
- Daemon now refuses to start if multiple .db files exist (ambiguity error)
- Updated tests to expect beads.db instead of prefix-based naming
- Tested migration, ambiguity detection, and warning messages
This commit is contained in:
33
beads.go
33
beads.go
@@ -9,6 +9,7 @@ package beads
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
@@ -162,6 +163,7 @@ type DatabaseInfo struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// findDatabaseInTree walks up the directory tree looking for .beads/*.db
|
// findDatabaseInTree walks up the directory tree looking for .beads/*.db
|
||||||
|
// Prefers beads.db and returns an error (via stderr warning) if multiple .db files exist
|
||||||
func findDatabaseInTree() string {
|
func findDatabaseInTree() string {
|
||||||
dir, err := os.Getwd()
|
dir, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -172,11 +174,38 @@ func findDatabaseInTree() string {
|
|||||||
for {
|
for {
|
||||||
beadsDir := filepath.Join(dir, ".beads")
|
beadsDir := filepath.Join(dir, ".beads")
|
||||||
if info, err := os.Stat(beadsDir); err == nil && info.IsDir() {
|
if info, err := os.Stat(beadsDir); err == nil && info.IsDir() {
|
||||||
|
// Check for canonical beads.db first
|
||||||
|
canonicalDB := filepath.Join(beadsDir, "beads.db")
|
||||||
|
if _, err := os.Stat(canonicalDB); err == nil {
|
||||||
|
return canonicalDB
|
||||||
|
}
|
||||||
|
|
||||||
// Found .beads/ directory, look for *.db files
|
// Found .beads/ directory, look for *.db files
|
||||||
matches, err := filepath.Glob(filepath.Join(beadsDir, "*.db"))
|
matches, err := filepath.Glob(filepath.Join(beadsDir, "*.db"))
|
||||||
if err == nil && len(matches) > 0 {
|
if err == nil && len(matches) > 0 {
|
||||||
// Return first .db file found
|
// Filter out backup files
|
||||||
return matches[0]
|
var validDBs []string
|
||||||
|
for _, match := range matches {
|
||||||
|
baseName := filepath.Base(match)
|
||||||
|
// Skip backup files (e.g., beads.db.backup, bd.db.backup)
|
||||||
|
if filepath.Ext(baseName) != ".backup" {
|
||||||
|
validDBs = append(validDBs, match)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(validDBs) > 1 {
|
||||||
|
// Multiple databases found - this is ambiguous
|
||||||
|
// Print error to stderr but return the first one for backward compatibility
|
||||||
|
fmt.Fprintf(os.Stderr, "Warning: Multiple database files found in %s:\n", beadsDir)
|
||||||
|
for _, db := range validDBs {
|
||||||
|
fmt.Fprintf(os.Stderr, " - %s\n", filepath.Base(db))
|
||||||
|
}
|
||||||
|
fmt.Fprintf(os.Stderr, "Run 'bd init' to migrate to beads.db or manually remove old databases.\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(validDBs) > 0 {
|
||||||
|
return validDBs[0]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1069,6 +1069,27 @@ func runDaemonLoop(interval time.Duration, autoCommit, autoPush bool, logPath, p
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for multiple .db files (ambiguity error)
|
||||||
|
beadsDir := filepath.Dir(daemonDBPath)
|
||||||
|
matches, err := filepath.Glob(filepath.Join(beadsDir, "*.db"))
|
||||||
|
if err == nil && len(matches) > 1 {
|
||||||
|
// Filter out backup files
|
||||||
|
var validDBs []string
|
||||||
|
for _, match := range matches {
|
||||||
|
if filepath.Ext(filepath.Base(match)) != ".backup" {
|
||||||
|
validDBs = append(validDBs, match)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(validDBs) > 1 {
|
||||||
|
log.log("Error: Multiple database files found in %s:", beadsDir)
|
||||||
|
for _, db := range validDBs {
|
||||||
|
log.log(" - %s", filepath.Base(db))
|
||||||
|
}
|
||||||
|
log.log("Run 'bd init' to migrate to beads.db or manually remove old databases")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
log.log("Using database: %s", daemonDBPath)
|
log.log("Using database: %s", daemonDBPath)
|
||||||
|
|
||||||
store, err := sqlite.New(daemonDBPath)
|
store, err := sqlite.New(daemonDBPath)
|
||||||
@@ -1079,8 +1100,7 @@ func runDaemonLoop(interval time.Duration, autoCommit, autoPush bool, logPath, p
|
|||||||
defer func() { _ = store.Close() }()
|
defer func() { _ = store.Close() }()
|
||||||
log.log("Database opened: %s", daemonDBPath)
|
log.log("Database opened: %s", daemonDBPath)
|
||||||
|
|
||||||
// Get workspace path (.beads directory)
|
// Get workspace path (.beads directory) - beadsDir already defined above
|
||||||
beadsDir := filepath.Dir(daemonDBPath)
|
|
||||||
// Get actual workspace root (parent of .beads)
|
// Get actual workspace root (parent of .beads)
|
||||||
workspacePath := filepath.Dir(beadsDir)
|
workspacePath := filepath.Dir(beadsDir)
|
||||||
socketPath := filepath.Join(beadsDir, "bd.sock")
|
socketPath := filepath.Join(beadsDir, "bd.sock")
|
||||||
|
|||||||
@@ -44,10 +44,16 @@ and database file. Optionally specify a custom issue prefix.`,
|
|||||||
prefix = strings.TrimRight(prefix, "-")
|
prefix = strings.TrimRight(prefix, "-")
|
||||||
|
|
||||||
// Create database
|
// Create database
|
||||||
// Use global dbPath if set via --db flag or BEADS_DB env var, otherwise default to .beads/{prefix}.db
|
// Use global dbPath if set via --db flag or BEADS_DB env var, otherwise default to .beads/beads.db
|
||||||
initDBPath := dbPath
|
initDBPath := dbPath
|
||||||
if initDBPath == "" {
|
if initDBPath == "" {
|
||||||
initDBPath = filepath.Join(".beads", prefix+".db")
|
initDBPath = filepath.Join(".beads", "beads.db")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate old database files if they exist
|
||||||
|
if err := migrateOldDatabases(initDBPath, quiet); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error during database migration: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine if we should create .beads/ directory in CWD
|
// Determine if we should create .beads/ directory in CWD
|
||||||
@@ -355,3 +361,63 @@ exit 0
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// migrateOldDatabases detects and migrates old database files to beads.db
|
||||||
|
func migrateOldDatabases(targetPath string, quiet bool) error {
|
||||||
|
targetDir := filepath.Dir(targetPath)
|
||||||
|
targetName := filepath.Base(targetPath)
|
||||||
|
|
||||||
|
// If target already exists, no migration needed
|
||||||
|
if _, err := os.Stat(targetPath); err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create .beads directory if it doesn't exist
|
||||||
|
if err := os.MkdirAll(targetDir, 0750); err != nil {
|
||||||
|
return fmt.Errorf("failed to create .beads directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for existing .db files in the .beads directory
|
||||||
|
pattern := filepath.Join(targetDir, "*.db")
|
||||||
|
matches, err := filepath.Glob(pattern)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to search for existing databases: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter out the target file name and any backup files
|
||||||
|
var oldDBs []string
|
||||||
|
for _, match := range matches {
|
||||||
|
baseName := filepath.Base(match)
|
||||||
|
if baseName != targetName && !strings.HasSuffix(baseName, ".backup.db") {
|
||||||
|
oldDBs = append(oldDBs, match)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(oldDBs) == 0 {
|
||||||
|
// No old databases to migrate
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(oldDBs) > 1 {
|
||||||
|
// Multiple databases found - ambiguous, require manual intervention
|
||||||
|
return fmt.Errorf("multiple database files found in %s: %v\nPlease manually rename the correct database to %s and remove others",
|
||||||
|
targetDir, oldDBs, targetName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate the single old database
|
||||||
|
oldDB := oldDBs[0]
|
||||||
|
if !quiet {
|
||||||
|
fmt.Fprintf(os.Stderr, "→ Migrating database: %s → %s\n", filepath.Base(oldDB), targetName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rename the old database to the new canonical name
|
||||||
|
if err := os.Rename(oldDB, targetPath); err != nil {
|
||||||
|
return fmt.Errorf("failed to migrate database %s to %s: %w", oldDB, targetPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !quiet {
|
||||||
|
fmt.Fprintf(os.Stderr, "✓ Database migration complete\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -143,17 +143,8 @@ func TestInitCommand(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify database was created
|
// Verify database was created (always beads.db now)
|
||||||
var dbPath string
|
dbPath := filepath.Join(beadsDir, "beads.db")
|
||||||
if tt.prefix != "" {
|
|
||||||
expectedPrefix := strings.TrimRight(tt.prefix, "-")
|
|
||||||
dbPath = filepath.Join(beadsDir, expectedPrefix+".db")
|
|
||||||
} else {
|
|
||||||
// Should use directory name as prefix
|
|
||||||
dirName := filepath.Base(tmpDir)
|
|
||||||
dbPath = filepath.Join(beadsDir, dirName+".db")
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
|
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
|
||||||
t.Errorf("Database file was not created at %s", dbPath)
|
t.Errorf("Database file was not created at %s", dbPath)
|
||||||
}
|
}
|
||||||
@@ -228,8 +219,8 @@ func TestInitAlreadyInitialized(t *testing.T) {
|
|||||||
t.Fatalf("Second init failed: %v", err)
|
t.Fatalf("Second init failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify database still works
|
// Verify database still works (always beads.db now)
|
||||||
dbPath := filepath.Join(tmpDir, ".beads", "test.db")
|
dbPath := filepath.Join(tmpDir, ".beads", "beads.db")
|
||||||
store, err := sqlite.New(dbPath)
|
store, err := sqlite.New(dbPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to open database after re-init: %v", err)
|
t.Fatalf("Failed to open database after re-init: %v", err)
|
||||||
|
|||||||
Reference in New Issue
Block a user