JuliusBrussee/caveman · error

sqlite parent must be a directory

Error message

sqlite parent %q must be a directory

What it means

After resolving symlinks, PrepareSQLitePathCanonical stats the resolved parent and requires it to be a directory. This error is thrown when the resolved parent path exists but is a regular file, socket, fifo, or other non-directory entry, so a SQLite database cannot live inside it.

Solutions

  1. Check the resolved parent with `file` / `os.Stat` and confirm what it actually is
  2. Correct the configured path so the parent component is a real directory
  3. Rename/remove the offending file at that path and recreate the directory with os.MkdirAll
  4. If the path was produced by string concatenation, fix the join so the db filename is not nested under a file path

Example fix

// before
ccr.Open("/data/ccr.db/store.db") // /data/ccr.db is a file
// after
ccr.Open("/data/ccr/store.db") // parent /data/ccr is a directory
Defensive patterns

Strategy: validation

Validate before calling

func parentIsDir(dbPath string) error {
    fi, err := os.Stat(filepath.Dir(dbPath))
    if err != nil { return err }
    if !fi.IsDir() { return fmt.Errorf("%s is not a directory", filepath.Dir(dbPath)) }
    return nil
}

Try / catch

if err := ccr.Open(dbPath); err != nil && strings.Contains(err.Error(), "must be a directory") {
    // fix config or recreate the parent as a directory before retrying
    return fmt.Errorf("bad ccr path config: %w", err)
}

Prevention

When it happens

Trigger: Calling Open/PrepareSQLitePath with a path like /path/to/file.db/subdir/db.sqlite where an intermediate component is a file; pointing the db path at a location whose 'parent' is actually a file (e.g. configured path 'data.db/db.sqlite'); a mountpoint replaced a directory with a regular file.

Common situations: Config where the user set the db 'directory' option to the file path instead of its folder; a tar/backup restore replaced a directory with a file; container image layering where a file shadows an expected directory.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/fa95ab31858e9f69. Report an issue: GitHub.

Appendix: source

Thrown at engine/ccr/store_sqlite.go:301

	}
	if strings.TrimSpace(path) == "" {
		return "", fmt.Errorf("ccr sqlite path is required")
	}
	parent := filepath.Dir(path)
	absoluteParent, err := filepath.Abs(parent)
	if err != nil {
		return "", fmt.Errorf("resolve sqlite parent %q: %w", parent, err)
	}
	resolvedParent, err := filepath.EvalSymlinks(absoluteParent)
	if err != nil {
		return "", fmt.Errorf("resolve sqlite parent %q: %w", parent, err)
	}
	parentInfo, err := os.Stat(resolvedParent)
	if err != nil {
		return "", fmt.Errorf("inspect sqlite parent %q: %w", resolvedParent, err)
	}
	if !parentInfo.IsDir() {
		return "", fmt.Errorf("sqlite parent %q must be a directory", resolvedParent)
	}
	if err := validateSQLiteParentSecurity(resolvedParent, parentInfo); err != nil {
		return "", err
	}
	canonicalPath := filepath.Join(resolvedParent, filepath.Base(path))
	if err := secureSQLiteFile(canonicalPath, true); err != nil {
		return "", fmt.Errorf("secure sqlite %q: %w", canonicalPath, err)
	}
	for _, suffix := range []string{"-wal", "-shm"} {
		if err := secureSQLiteFile(canonicalPath+suffix, false); err != nil {
			return "", fmt.Errorf("secure sqlite sidecar %q: %w", canonicalPath+suffix, err)
		}
	}
	return canonicalPath, nil
}

func secureSQLiteFiles(path string) error {
	if path == ":memory:" {

View on GitHub (pinned to 3ee70a1026)