JuliusBrussee/caveman · error

: database parent changed

Error message

%w: database parent changed

What it means

inspectSQLiteGeneration (engine/ccr/store_generation.go:46) rejects the database when its parent directory's symlink-resolved path differs from the expected spelling, or when EvalSymlinks reports the parent missing. This is a deliberate security check: it catches the parent directory (or an intermediate component) being swapped for a symlink since the path was canonicalized, which os.SameFile alone cannot detect. Unlike error 1038, this means the environment HAS changed (or the parent vanished) — the store is treated as ErrStorageChanged without the errStorageUnverifiable qualifier.

Solutions

  1. Check whether ~/.caveman (or the configured CCR parent) is now a symlink — restore it to a real directory or point the store at the canonical target.
  2. If the parent directory was deleted or moved, recreate it at the original canonical path or reinitialize the store at the new location.
  3. Always construct the database path via PrepareSQLitePathCanonical; the check compares spellings on purpose and rejects non-canonical paths.
  4. Treat this as ErrStorageChanged (not unverifiable): if a terminal quarantine decision triggered, follow the store's quarantine/recovery procedure rather than retrying blindly.
  5. Audit what changed in the environment between open and check (mount changes, container restarts, symlink-swap tools) before recreating the store.

Example fix

// before
// ~/.caveman replaced by symlink to ~/dotfiles/caveman → parent spelling changes
store := ccr.Open("/home/me/.caveman/ccr.db") // error: storage changed: database parent changed

// after
rm ~/.caveman
mkdir -p ~/.caveman   # real directory, or exclude it from the dotfile symlink manager
store := ccr.Open("/home/me/.caveman/ccr.db")
Defensive patterns

Strategy: try-catch

Validate before calling

func assertCanonicalCCRParent(path string) error {
	parent := filepath.Dir(path)
	resolved, err := filepath.EvalSymlinks(parent)
	if err != nil {
		return fmt.Errorf("ccr parent missing or unresolvable: %w", err)
	}
	if resolved != parent {
		return fmt.Errorf("%s contains a symlink component (resolves to %s)", parent, resolved)
	}
	return nil
}
// run at startup and before every checkGeneration-sensitive operation

Try / catch

files, err := inspectSQLiteGeneration(path)
if err != nil {
	if errors.Is(err, ErrStorageChanged) && !errors.Is(err, errStorageUnverifiable) {
		// confirmed environment change (parent swapped/removed) — treat the
		// store identity as changed and follow the quarantine/recovery path
		return fmt.Errorf("ccr storage changed, manual recovery required: %w", err)
	}
	return err
}

Prevention

When it happens

Trigger: Calling checkGeneration (via openWithBudgetHooks, secureSQLiteGeneration flows, or the inspection test) when: (a) filepath.EvalSymlinks returns os.ErrNotExist because the parent directory was deleted/moved after the store was opened, or (b) the resolved path string differs from the canonical parent — i.e. some component became a symlink, or a non-canonical path spelling was passed in.

Common situations: A dotfile manager or backup tool replaced ~/.caveman with a symlink; the data directory was moved between inspection passes; /tmp or home redirection changed (TMPDIR, container restarts with different mounts); the store path was constructed by hand with ../ or double slashes instead of via PrepareSQLitePathCanonical.

Understand the failure class

Background: "File not found" and ENOENT errors: why libraries can't find a file that should exist — this error's family across 50 libraries.

Related errors


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

Appendix: source

Thrown at engine/ccr/store_generation.go:46

var errStorageUnverifiable = errors.New("storage identity could not be verified")

// inspectSQLiteGeneration requires the canonical path PrepareSQLitePathCanonical
// returned. The parent check below is a re-verification that no component became
// a symlink since; it compares spellings on purpose, because following a swapped
// intermediate symlink yields the same directory identity and so cannot be
// detected by os.SameFile. A non-canonical spelling is reported as a change.
func inspectSQLiteGeneration(path string) (sqliteGeneration, error) {
	var files sqliteGeneration
	if path == ":memory:" {
		return files, nil
	}
	parent := filepath.Dir(path)
	resolved, err := filepath.EvalSymlinks(parent)
	if err != nil && !errors.Is(err, os.ErrNotExist) {
		return files, fmt.Errorf("%w: %w: inspect database parent: %v", ErrStorageChanged, errStorageUnverifiable, err)
	}
	if err != nil || resolved != parent {
		return files, fmt.Errorf("%w: database parent changed", ErrStorageChanged)
	}
	info, err := os.Stat(parent)
	if errors.Is(err, os.ErrNotExist) {
		return files, fmt.Errorf("%w: database parent changed", ErrStorageChanged)
	}
	if err != nil {
		return files, fmt.Errorf("%w: %w: inspect database parent: %v", ErrStorageChanged, errStorageUnverifiable, err)
	}
	if err := validateSQLiteParentSecurity(parent, info); err != nil {
		return files, fmt.Errorf("%w: %w: %v", ErrStorageChanged, errStorageUnverifiable, err)
	}
	for i, suffix := range sqliteSuffixes {
		info, err := inspectSQLiteFile(path + suffix)
		if errors.Is(err, os.ErrNotExist) && i != 0 {
			continue
		}
		if errors.Is(err, os.ErrNotExist) {
			return files, fmt.Errorf("%w: inspect database%s: %v", ErrStorageChanged, suffix, err)

View on GitHub (pinned to 3ee70a1026)