go-sqlite-default-path
The divergence: config.DefaultSQLitePath was ./memory.db while the docs promised ~/.dexdat-memory/memory.db. Four coordinated changes align code to docs without weakening explicit configuration.
1. Default path (config.go) — default to the documented home-relative path, expand ~ at load time:
const DefaultSQLitePath = "~/.dexdat-memory/memory.db"
func expandHome(p string) (string, error) {
if p == "~" || strings.HasPrefix(p, "~/") {
home, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("resolve home directory: %w", err)
}
if p == "~" {
return home, nil
}
return filepath.Join(home, strings.TrimPrefix(p, "~/")), nil
}
return p, nil // explicit relative/absolute paths stay as the operator wrote them
}
2. Explicit-vs-default detection — a config struct carries IsDefault, computed from the config file (v.InConfig, not v.IsSet — viper's IsSet reports true for keys that only have a default) plus a direct SQLITE_DB_PATH env check. An empty SQLITE_DB_PATH counts as unset (matches viper's default allow-empty-env=false):
v.SetDefault("sqlite.path", DefaultSQLitePath)
_ = v.BindEnv("sqlite.path", EnvSQLitePath) // SQLITE_DB_PATH
explicit := os.Getenv(EnvSQLitePath) != "" || v.InConfig("sqlite.path")
cfg := &Config{SQLite: SQLiteConfig{Path: expanded, IsDefault: !explicit}}
3. Auto-create only for zero-config; fail-fast otherwise (sqlite.go) — MkdirAll guarded by IsDefault; explicit paths that point at a missing directory error at startup instead of silently creating directories the operator never asked for:
func Open(ctx context.Context, cfg *config.Config, log *slog.Logger) (*sql.DB, error) {
if cfg.SQLite.IsDefault {
dir := filepath.Dir(cfg.SQLite.Path)
if err := os.MkdirAll(dir, 0o700); err != nil {
return nil, fmt.Errorf("create data dir %q: %w", dir, err)
}
}
// ... sql.Open + PingContext as before
}
4. Startup log (main.go) — the canonical "where is my data" answer:
log.Info("starting dexdat-memory",
"sqlite_path", cfg.SQLite.Path,
"sqlite_path_is_default", cfg.SQLite.IsDefault,
)
5. viper v1.19 missing-config quirk — with SetConfigFile on a missing file, v1.19 returns a raw *fs.PathError (*os.PathError is an alias) instead of ConfigFileNotFoundError; treat both as "no config present", anything else is a real error:
if err := v.ReadInConfig(); err != nil {
var notFound viper.ConfigFileNotFoundError
var pathErr *fs.PathError
switch {
case errors.As(err, ¬Found), errors.As(err, &pathErr):
// no config file: zero-config mode, defaults apply
default:
return nil, fmt.Errorf("read config: %w", err)
}
}
6. README — added a Data location section documenting ~/.dexdat-memory/memory.db as default, SQLITE_DB_PATH/sqlite.path override, the auto-create-only-for-default rule, and the startup log line.
Built and verified in `/tmp/dexdat-fix` (Go 1.26, viper v1.19.0, modernc sqlite), 8 tests, `go vet` clean, `gofmt` clean.
**Unit tests (all pass, `-count=1`):**
- `TestLoadDefaultPath` — no env/config → `Path == $HOME/.dexdat-memory/memory.db`, `IsDefault=true`
- `TestLoadExplicitEnvPath` — `SQLITE_DB_PATH` set → custom path, `IsDefault=false`
- `TestLoadExplicitConfigFilePath` — `dexdat.yaml` with `sqlite.path` → wins, `IsDefault=false`
- `TestLoadNoConfigFileIsNotAnError` — the v1.19 quirk path: Load succeeds with zero config files
- `TestExpandHome` — `~`, `~/x`, `./memory.db`, absolute paths all expand/resolve correctly
- `TestOpenDefaultAutoCreatesDir` — dir auto-created, DB file usable, **cwd contains 0 files**
- `TestOpenExplicitFailsFast` — missing parent dir → error, directory NOT created
- `TestOpenExplicitExistingDirSucceeds` — valid explicit dir works; table round-trip OK
**End-to-end runs of the built binary (fresh worktree, fake HOME):**
1. Zero-config: logged `sqlite_path=/tmp/.../fakehome/.dexdat-memory/memory.db sqlite_path_is_default=true`; `~/.dexdat-memory/memory.db` created; worktree stayed empty (0 files) — the litter bug is gone.
2. `SQLITE_DB_PATH=/tmp/dexdat-fix/nonexistent/mem.db` → `ERROR ... unable to open database file (14)`, exit=1, and the directory was *not* auto-created (fail-fast preserved).
3. `SQLITE_DB_PATH` with an existing dir → starts cleanly and the DB works.
**viper v1.19 quirk reproduced directly:**
- `AddConfigPath` + missing file → `viper.ConfigFileNotFoundError` (typed, old behavior)
- `SetConfigFile` + missing file → `*fs.PathError: open ... no such file or directory`, `is ConfigFileNotFoundError: false` — confirming the regression; the dual `errors.As` in Load handles both, which is why `TestLoadNoConfigFileIsNotAnError` passes.
One subtlety caught during implementation: `v.IsSet("sqlite.path")` returns `true` for default-only keys, so explicit detection must use `v.InConfig` + env lookup — without this, every zero-config run would have been mislabeled explicit and auto-creation would never fire.{"model": "deepseek-v4-flash", "problem_class": "go-sqlite-default-path", "result": "passed", "tests": 8}