From 64742cd5740b54f4892ec7cbb949761c9b49647b Mon Sep 17 00:00:00 2001 From: Matteo Landi Date: Fri, 7 Nov 2025 23:19:14 +0100 Subject: [PATCH 1/2] Fix SQLite driver name mismatch causing "unknown driver" errors (#252) * fix: Use correct SQLite driver name 'sqlite3' instead of 'sqlite' The ncruces/go-sqlite3 driver registers as 'sqlite3', but doctor.go and example code were using 'sqlite', causing 'unknown driver' errors. This fix corrects all sql.Open() calls to use the proper driver name: - cmd/bd/doctor.go: 6 instances fixed - docs/EXTENDING.md: 2 documentation examples updated - examples/bd-example-extension-go/: Fixed example code and README Fixes #230 Amp-Thread-ID: https://ampcode.com/threads/T-1e8c5473-cb79-4457-be07-4517bfdb73f4 Co-authored-by: Amp * Revert CGO_ENABLED back to 0 for pure-Go SQLite driver The ncruces/go-sqlite3 driver is pure-Go and doesn't require CGO. The previous change to CGO_ENABLED=1 in commit f9771cd was an attempted fix for #230, but the real issue was the driver name mismatch ('sqlite' vs 'sqlite3'), which is now fixed. Benefits of CGO_ENABLED=0: - Simpler cross-compilation (no C toolchain required) - Smaller binaries - Faster builds - Matches the intended design of the pure-Go driver --------- Co-authored-by: Amp --- .goreleaser.yml | 2 +- cmd/bd/doctor.go | 10 +++++----- docs/EXTENDING.md | 4 ++-- examples/bd-example-extension-go/README.md | 2 +- examples/bd-example-extension-go/main.go | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index cac03e36..e66992e1 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -13,7 +13,7 @@ builds: main: ./cmd/bd binary: bd env: - - CGO_ENABLED=1 + - CGO_ENABLED=0 goos: - linux - darwin diff --git a/cmd/bd/doctor.go b/cmd/bd/doctor.go index b8949cae..1f270c59 100644 --- a/cmd/bd/doctor.go +++ b/cmd/bd/doctor.go @@ -314,7 +314,7 @@ func checkIDFormat(path string) doctorCheck { } // Open database - db, err := sql.Open("sqlite", dbPath+"?mode=ro") + db, err := sql.Open("sqlite3", dbPath+"?mode=ro") if err != nil { return doctorCheck{ Name: "Issue IDs", @@ -400,7 +400,7 @@ func checkCLIVersion() doctorCheck { } func getDatabaseVersionFromPath(dbPath string) string { - db, err := sql.Open("sqlite", dbPath+"?mode=ro") + db, err := sql.Open("sqlite3", dbPath+"?mode=ro") if err != nil { return "unknown" } @@ -785,7 +785,7 @@ func checkDatabaseJSONLSync(path string) doctorCheck { jsonlCount, jsonlPrefixes, jsonlErr := countJSONLIssues(jsonlPath) // Single database open for all queries (instead of 3 separate opens) - db, err := sql.Open("sqlite", dbPath) + db, err := sql.Open("sqlite3", dbPath) if err != nil { // Database can't be opened. If JSONL has issues, suggest recovery. if jsonlErr == nil && jsonlCount > 0 { @@ -990,7 +990,7 @@ func checkPermissions(path string) doctorCheck { dbPath := filepath.Join(beadsDir, beads.CanonicalDatabaseName) if _, err := os.Stat(dbPath); err == nil { // Try to open database - db, err := sql.Open("sqlite", dbPath) + db, err := sql.Open("sqlite3", dbPath) if err != nil { return doctorCheck{ Name: "Permissions", @@ -1038,7 +1038,7 @@ func checkDependencyCycles(path string) doctorCheck { } // Open database to check for cycles - db, err := sql.Open("sqlite", dbPath) + db, err := sql.Open("sqlite3", dbPath) if err != nil { return doctorCheck{ Name: "Dependency Cycles", diff --git a/docs/EXTENDING.md b/docs/EXTENDING.md index aa7e9d9e..e7ffa1a0 100644 --- a/docs/EXTENDING.md +++ b/docs/EXTENDING.md @@ -84,7 +84,7 @@ CREATE INDEX IF NOT EXISTS idx_checkpoints_execution ON myapp_checkpoints(execut ` func InitializeMyAppSchema(dbPath string) error { - db, err := sql.Open("sqlite", dbPath) + db, err := sql.Open("sqlite3", dbPath) if err != nil { return err } @@ -572,7 +572,7 @@ if dbPath == "" { } // Open your own connection to the same database -db, err := sql.Open("sqlite", dbPath) +db, err := sql.Open("sqlite3", dbPath) if err != nil { log.Fatal(err) } diff --git a/examples/bd-example-extension-go/README.md b/examples/bd-example-extension-go/README.md index dc3a2c32..818ec5ae 100644 --- a/examples/bd-example-extension-go/README.md +++ b/examples/bd-example-extension-go/README.md @@ -182,7 +182,7 @@ jsonlPath := beads.FindJSONLPath(dbPath) ```go // Open same database for extension tables -db, err := sql.Open("sqlite", dbPath) +db, err := sql.Open("sqlite3", dbPath) // Initialize extension schema _, err = db.Exec(Schema) diff --git a/examples/bd-example-extension-go/main.go b/examples/bd-example-extension-go/main.go index 725b30f4..54fe37ab 100644 --- a/examples/bd-example-extension-go/main.go +++ b/examples/bd-example-extension-go/main.go @@ -30,7 +30,7 @@ func main() { // Open bd storage + extension database store, _ := beads.NewSQLiteStorage(*dbPath) defer store.Close() - db, _ := sql.Open("sqlite", *dbPath) + db, _ := sql.Open("sqlite3", *dbPath) defer db.Close() db.Exec("PRAGMA journal_mode=WAL") db.Exec("PRAGMA busy_timeout=5000") From 6408ef65f4a3fc82fc17fd51c6cb0fefd53f3c87 Mon Sep 17 00:00:00 2001 From: vector-sigma Date: Fri, 7 Nov 2025 17:21:14 -0500 Subject: [PATCH 2/2] Fix #249: Add nil storage checks to prevent RPC daemon crashes (#250) The daemon RPC server was crashing with a nil pointer dereference when the global daemon received list, ready, stats, or other storage-dependent RPC requests. The global daemon is created with nil storage, causing these operations to panic when they attempted to access storage methods. This fix adds defensive nil checks at the beginning of all RPC handlers that require storage access. When storage is unavailable, they now return a proper JSON error response instead of crashing the daemon. The error message also informs users that the global daemon is deprecated and they should use local daemons instead. Handlers fixed: - handleCreate, handleUpdate, handleClose - handleList, handleShow, handleReady, handleStale - handleResolveID, handleStats, handleEpicStatus - handleCompact, handleCompactStats - handleDepAdd (and via handleSimpleStoreOp for all label/dep/comment ops) Co-authored-by: Test User --- internal/rpc/server_compact.go | 12 ++++ internal/rpc/server_issues_epics.go | 61 +++++++++++++++++++++ internal/rpc/server_labels_deps_comments.go | 12 ++++ 3 files changed, 85 insertions(+) diff --git a/internal/rpc/server_compact.go b/internal/rpc/server_compact.go index e1b8411f..864fb630 100644 --- a/internal/rpc/server_compact.go +++ b/internal/rpc/server_compact.go @@ -19,6 +19,12 @@ func (s *Server) handleCompact(req *Request) Response { } store := s.storage + if store == nil { + return Response{ + Success: false, + Error: "storage not available (global daemon deprecated - use local daemon instead with 'bd daemon' in your project)", + } + } sqliteStore, ok := store.(*sqlite.SQLiteStorage) if !ok { @@ -228,6 +234,12 @@ func (s *Server) handleCompactStats(req *Request) Response { } store := s.storage + if store == nil { + return Response{ + Success: false, + Error: "storage not available (global daemon deprecated - use local daemon instead with 'bd daemon' in your project)", + } + } sqliteStore, ok := store.(*sqlite.SQLiteStorage) if !ok { diff --git a/internal/rpc/server_issues_epics.go b/internal/rpc/server_issues_epics.go index 5248cb04..e7a17e95 100644 --- a/internal/rpc/server_issues_epics.go +++ b/internal/rpc/server_issues_epics.go @@ -87,6 +87,12 @@ func (s *Server) handleCreate(req *Request) Response { } store := s.storage + if store == nil { + return Response{ + Success: false, + Error: "storage not available (global daemon deprecated - use local daemon instead with 'bd daemon' in your project)", + } + } ctx := s.reqCtx(req) // If parent is specified, generate child ID @@ -242,6 +248,12 @@ func (s *Server) handleUpdate(req *Request) Response { } store := s.storage + if store == nil { + return Response{ + Success: false, + Error: "storage not available (global daemon deprecated - use local daemon instead with 'bd daemon' in your project)", + } + } ctx := s.reqCtx(req) updates := updatesFromArgs(updateArgs) @@ -284,6 +296,12 @@ func (s *Server) handleClose(req *Request) Response { } store := s.storage + if store == nil { + return Response{ + Success: false, + Error: "storage not available (global daemon deprecated - use local daemon instead with 'bd daemon' in your project)", + } + } ctx := s.reqCtx(req) if err := store.CloseIssue(ctx, closeArgs.ID, closeArgs.Reason, s.reqActor(req)); err != nil { @@ -314,6 +332,12 @@ func (s *Server) handleList(req *Request) Response { } store := s.storage + if store == nil { + return Response{ + Success: false, + Error: "storage not available (global daemon deprecated - use local daemon instead with 'bd daemon' in your project)", + } + } filter := types.IssueFilter{ Limit: listArgs.Limit, @@ -492,6 +516,13 @@ func (s *Server) handleResolveID(req *Request) Response { } } + if s.storage == nil { + return Response{ + Success: false, + Error: "storage not available (global daemon deprecated - use local daemon instead with 'bd daemon' in your project)", + } + } + ctx := s.reqCtx(req) resolvedID, err := utils.ResolvePartialID(ctx, s.storage, args.ID) if err != nil { @@ -518,6 +549,12 @@ func (s *Server) handleShow(req *Request) Response { } store := s.storage + if store == nil { + return Response{ + Success: false, + Error: "storage not available (global daemon deprecated - use local daemon instead with 'bd daemon' in your project)", + } + } ctx := s.reqCtx(req) issue, err := store.GetIssue(ctx, showArgs.ID) @@ -593,6 +630,12 @@ func (s *Server) handleReady(req *Request) Response { } store := s.storage + if store == nil { + return Response{ + Success: false, + Error: "storage not available (global daemon deprecated - use local daemon instead with 'bd daemon' in your project)", + } + } wf := types.WorkFilter{ Status: types.StatusOpen, @@ -632,6 +675,12 @@ func (s *Server) handleStale(req *Request) Response { } store := s.storage + if store == nil { + return Response{ + Success: false, + Error: "storage not available (global daemon deprecated - use local daemon instead with 'bd daemon' in your project)", + } + } filter := types.StaleFilter{ Days: staleArgs.Days, @@ -657,6 +706,12 @@ func (s *Server) handleStale(req *Request) Response { func (s *Server) handleStats(req *Request) Response { store := s.storage + if store == nil { + return Response{ + Success: false, + Error: "storage not available (global daemon deprecated - use local daemon instead with 'bd daemon' in your project)", + } + } ctx := s.reqCtx(req) stats, err := store.GetStatistics(ctx) @@ -684,6 +739,12 @@ func (s *Server) handleEpicStatus(req *Request) Response { } store := s.storage + if store == nil { + return Response{ + Success: false, + Error: "storage not available (global daemon deprecated - use local daemon instead with 'bd daemon' in your project)", + } + } ctx := s.reqCtx(req) epics, err := store.GetEpicsEligibleForClosure(ctx) diff --git a/internal/rpc/server_labels_deps_comments.go b/internal/rpc/server_labels_deps_comments.go index 345ed280..e48f90ef 100644 --- a/internal/rpc/server_labels_deps_comments.go +++ b/internal/rpc/server_labels_deps_comments.go @@ -19,6 +19,12 @@ func (s *Server) handleDepAdd(req *Request) Response { } store := s.storage + if store == nil { + return Response{ + Success: false, + Error: "storage not available (global daemon deprecated - use local daemon instead with 'bd daemon' in your project)", + } + } dep := &types.Dependency{ IssueID: depArgs.FromID, @@ -51,6 +57,12 @@ func (s *Server) handleSimpleStoreOp(req *Request, argsPtr interface{}, argDesc } store := s.storage + if store == nil { + return Response{ + Success: false, + Error: "storage not available (global daemon deprecated - use local daemon instead with 'bd daemon' in your project)", + } + } ctx := s.reqCtx(req) if err := opFunc(ctx, store, s.reqActor(req)); err != nil {