Add --no-db mode: JSONL-only operation without SQLite
Implement --no-db mode to avoid SQLite database corruption in scenarios
where the same .beads directory is accessed from multiple processes
(e.g., host + container, multiple containers).
Changes:
- Add in-memory storage backend (internal/storage/memory/memory.go)
- Implements full Storage interface using in-memory data structures
- Thread-safe with mutex protection for concurrent access
- Supports all core operations: issues, dependencies, labels, comments
- Add JSONL persistence layer (cmd/bd/nodb.go)
- initializeNoDbMode(): Load .beads/issues.jsonl on startup
- writeIssuesToJSONL(): Atomic write-back after each command
- detectPrefix(): Smart prefix detection with fallback hierarchy
1. .beads/nodb_prefix.txt (explicit config)
2. Common prefix from existing issues
3. Current directory name (fallback)
- Integrate --no-db flag into command flow (cmd/bd/main.go)
- Add global --no-db flag to all commands
- PersistentPreRun: Initialize memory storage from JSONL
- PersistentPostRun: Write memory back to JSONL atomically
- Skip daemon and SQLite initialization in --no-db mode
- Extract common writeJSONLAtomic() helper to eliminate duplication
- Update bd init for --no-db mode (cmd/bd/init.go)
- Create .beads/nodb_prefix.txt instead of SQLite database
- Create empty issues.jsonl file
- Display --no-db specific initialization message
Code Quality:
- Refactored atomic JSONL writes into shared writeJSONLAtomic() helper
- Used by both flushToJSONL (SQLite mode) and writeIssuesToJSONL (--no-db mode)
- Eliminates ~90 lines of code duplication
- Ensures consistent atomic write behavior across modes
Usage:
bd --no-db init -p myproject
bd --no-db create "Fix bug" --priority 1
bd --no-db list
bd --no-db update myproject-1 --status in_progress
Benefits:
- No SQLite corruption from concurrent access
- Container-safe: perfect for multi-mount scenarios
- Git-friendly: direct JSONL diffs work seamlessly
- Simple: no daemon, no WAL files, just JSONL
Test Results (go test ./...):
- ✓ github.com/steveyegge/beads: PASS
- ✗ github.com/steveyegge/beads/cmd/bd: 1 pre-existing failure (TestAutoFlushErrorHandling)
- ✓ github.com/steveyegge/beads/internal/compact: PASS
- ✗ github.com/steveyegge/beads/internal/rpc: 1 pre-existing failure (TestMemoryPressureDetection)
- ✓ github.com/steveyegge/beads/internal/storage/sqlite: PASS
- ✓ github.com/steveyegge/beads/internal/types: PASS
- ⚠ github.com/steveyegge/beads/internal/storage/memory: no tests yet
All test failures are pre-existing and unrelated to --no-db implementation.
The new --no-db mode has been manually tested and verified working.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
via [Happy](https://happy.engineering)
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
This commit is contained in:
@@ -560,6 +560,9 @@ func TestAutoFlushErrorHandling(t *testing.T) {
|
||||
t.Skip("chmod-based read-only directory behavior is not reliable on Windows")
|
||||
}
|
||||
|
||||
// Note: We create issues.jsonl as a directory to force os.Create() to fail,
|
||||
// which works even when running as root (unlike chmod-based approaches)
|
||||
|
||||
// Create temp directory for test database
|
||||
tmpDir, err := os.MkdirTemp("", "bd-test-error-*")
|
||||
if err != nil {
|
||||
@@ -601,16 +604,34 @@ func TestAutoFlushErrorHandling(t *testing.T) {
|
||||
t.Fatalf("Failed to create issue: %v", err)
|
||||
}
|
||||
|
||||
// Create a read-only directory to force flush failure
|
||||
readOnlyDir := filepath.Join(tmpDir, "readonly")
|
||||
if err := os.MkdirAll(readOnlyDir, 0555); err != nil {
|
||||
t.Fatalf("Failed to create read-only dir: %v", err)
|
||||
// Mark issue as dirty so flushToJSONL will try to export it
|
||||
if err := testStore.MarkIssueDirty(ctx, issue.ID); err != nil {
|
||||
t.Fatalf("Failed to mark issue dirty: %v", err)
|
||||
}
|
||||
defer os.Chmod(readOnlyDir, 0755) // Restore permissions for cleanup
|
||||
|
||||
// Set dbPath to point to read-only directory
|
||||
// Create a directory where the JSONL file should be, to force write failure
|
||||
// os.Create() will fail when trying to create a file with a path that's already a directory
|
||||
failDir := filepath.Join(tmpDir, "faildir")
|
||||
if err := os.MkdirAll(failDir, 0755); err != nil {
|
||||
t.Fatalf("Failed to create fail dir: %v", err)
|
||||
}
|
||||
|
||||
// Create issues.jsonl as a directory (not a file) to force Create() to fail
|
||||
jsonlAsDir := filepath.Join(failDir, "issues.jsonl")
|
||||
if err := os.MkdirAll(jsonlAsDir, 0755); err != nil {
|
||||
t.Fatalf("Failed to create issues.jsonl as directory: %v", err)
|
||||
}
|
||||
|
||||
// Set dbPath to point to faildir
|
||||
originalDBPath := dbPath
|
||||
dbPath = filepath.Join(readOnlyDir, "test.db")
|
||||
dbPath = filepath.Join(failDir, "test.db")
|
||||
|
||||
// Verify issue is actually marked as dirty
|
||||
dirtyIDs, err := testStore.GetDirtyIssues(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get dirty issues: %v", err)
|
||||
}
|
||||
t.Logf("Dirty issues before flush: %v", dirtyIDs)
|
||||
|
||||
// Reset failure counter
|
||||
flushMutex.Lock()
|
||||
@@ -619,6 +640,9 @@ func TestAutoFlushErrorHandling(t *testing.T) {
|
||||
isDirty = true
|
||||
flushMutex.Unlock()
|
||||
|
||||
t.Logf("dbPath set to: %s", dbPath)
|
||||
t.Logf("Expected JSONL path (which is a directory): %s", filepath.Join(failDir, "issues.jsonl"))
|
||||
|
||||
// Attempt flush (should fail)
|
||||
flushToJSONL()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user