Skip to content

Commit bd6adb6

Browse files
aksOpsclaude
andcommitted
fix(store): mkdir cost-DB parent dir on OpenCostStore
mattn/go-sqlite3 surfaces a missing parent dir as the unhelpful "unable to open database file: no such file or directory" error. Production opens ctm.db at ~/.config/ctm/ctm.db — on a fresh install or a CI runner with no pre-existing config dir, the parent doesn't exist and OpenCostStore fails before doing anything useful. Add a defensive os.MkdirAll(filepath.Dir(path), 0o700) at the top of OpenCostStore. Skipped for the in-memory path (":memory:" → Dir returns "." which is a no-op anyway, but explicit guard reads better). Mkdir errors are swallowed — if mkdir fails we let sql.Open surface the real problem instead of masking it. Side benefit: server_test.go's TestHealthz* / TestAuth* etc. were flakily passing or failing depending on whether earlier-running test packages (alphabetical: internal/config, …) created ~/.config/ctm/ as a side effect. CI runs without a populated test cache hit this regularly. Now they're independent. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 5e54a32 commit bd6adb6

1 file changed

Lines changed: 14 additions & 0 deletions

File tree

internal/serve/store/cost_store.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ import (
3434
"errors"
3535
"fmt"
3636
"net/url"
37+
"os"
38+
"path/filepath"
3739
"sync"
3840
"time"
3941

@@ -92,6 +94,18 @@ type sqliteCostStore struct {
9294
// keeps the handler-side Writer from erroring under light contention
9395
// with the quota-subscriber goroutine.
9496
func OpenCostStore(path string) (CostStore, error) {
97+
// Ensure the parent directory exists. Production opens the DB at
98+
// ~/.config/ctm/ctm.db; on a fresh install (or in CI runners
99+
// without a pre-existing config dir) the parent might not exist
100+
// yet, and mattn/go-sqlite3 surfaces that as
101+
// "unable to open database file". Cheap and idempotent — no-op
102+
// for ":memory:" (filepath.Dir returns "."). Errors here are
103+
// non-fatal: if mkdir fails we let sql.Open surface the real
104+
// problem.
105+
if path != ":memory:" {
106+
_ = os.MkdirAll(filepath.Dir(path), 0o700)
107+
}
108+
95109
// DSN tuning: ?_busy_timeout=5000 waits out brief writer locks;
96110
// ?_journal=WAL enables concurrent readers; ?_sync=NORMAL pairs
97111
// with WAL for an acceptable durability-vs-speed trade.

0 commit comments

Comments
 (0)