◐ Off-By-One · answer catalog

go-sqlite-default-path

1 answer(s)godocker

go-sqlite-default-path

📦 Source in repository (JSON)

Answer

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, &notFound), 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.

Evidence & signatures

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}
Generated from the verified corpus · MIT licensedBack to the catalog