From e972f1d8665b00eb0b42014976fcb478c8787d22 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Mon, 3 Nov 2025 16:16:27 -0800 Subject: [PATCH] fix(sqlite): Fix connection string construction for :memory: databases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous code had two bugs: 1. Double 'file:' prefix when path was ':memory:' 2. Two '?' separators instead of proper '?...&...' syntax This caused SQLite errors: 'no such cache mode: shared?_pragma=...' Fixed by: - Building connStr directly for :memory: case with proper syntax - Using '&' to chain query parameters - Handling filepath.Abs() only for real files, not :memory: 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- internal/storage/sqlite/sqlite.go | 41 ++++++++++++++----------------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/internal/storage/sqlite/sqlite.go b/internal/storage/sqlite/sqlite.go index 1e155efc..47dd4726 100644 --- a/internal/storage/sqlite/sqlite.go +++ b/internal/storage/sqlite/sqlite.go @@ -27,31 +27,22 @@ type SQLiteStorage struct { // New creates a new SQLite storage backend func New(path string) (*SQLiteStorage, error) { - // Convert :memory: to shared memory URL for consistent behavior across connections - // SQLite creates separate in-memory databases for each connection to ":memory:", - // but "file::memory:?cache=shared" creates a shared in-memory database. - dbPath := path + // Build connection string with proper URI syntax + // For :memory: databases, use shared cache so multiple connections see the same data + var connStr string if path == ":memory:" { - dbPath = "file::memory:?cache=shared" - } - - // Ensure directory exists (skip for memory databases) - if !strings.Contains(dbPath, ":memory:") { - dir := filepath.Dir(dbPath) + // Use shared in-memory database with pragmas + connStr = "file::memory:?cache=shared&_pragma=journal_mode(WAL)&_pragma=foreign_keys(ON)&_pragma=busy_timeout(30000)&_time_format=sqlite" + } else { + // Ensure directory exists for file-based databases + dir := filepath.Dir(path) if err := os.MkdirAll(dir, 0o750); err != nil { return nil, fmt.Errorf("failed to create directory: %w", err) } + // Use file URI with pragmas + connStr = "file:" + path + "?_pragma=journal_mode(WAL)&_pragma=foreign_keys(ON)&_pragma=busy_timeout(30000)&_time_format=sqlite" } - // Open database with WAL mode for better concurrency and busy timeout for parallel writes - // Use modernc.org/sqlite's _pragma syntax for all options to ensure consistent behavior - // _pragma=journal_mode(WAL) enables Write-Ahead Logging for better concurrency - // _pragma=foreign_keys(ON) enforces foreign key constraints - // _pragma=busy_timeout(30000) means wait up to 30 seconds for locks instead of failing immediately - // _time_format=sqlite enables automatic parsing of DATETIME columns to time.Time - // Use file: URI scheme to properly separate file path from query parameters - connStr := "file:" + dbPath + "?_pragma=journal_mode(WAL)&_pragma=foreign_keys(ON)&_pragma=busy_timeout(30000)&_time_format=sqlite" - db, err := sql.Open("sqlite3", connStr) if err != nil { return nil, fmt.Errorf("failed to open database: %w", err) @@ -72,10 +63,14 @@ func New(path string) (*SQLiteStorage, error) { return nil, err } - // Convert to absolute path for consistency - absPath, err := filepath.Abs(path) - if err != nil { - return nil, fmt.Errorf("failed to get absolute path: %w", err) + // Convert to absolute path for consistency (but keep :memory: as-is) + absPath := path + if path != ":memory:" { + var err error + absPath, err = filepath.Abs(path) + if err != nil { + return nil, fmt.Errorf("failed to get absolute path: %w", err) + } } return &SQLiteStorage{