JuliusBrussee/caveman · critical

migrate sqlite %q: %w

Error message

migrate sqlite %q: %w

What it means

Returned by store.Open when executing the base schema against the (newly or existing) SQLite database fails. This is the first real DB touch: it surfaces unusable/corrupt files, permission problems on the DB path, an incompatible SQLite file (e.g. created by a newer/wal-mode incompatible tool), or objects in the way (a table/view named like a schema table).

Source

Thrown at proxy/internal/store/store.go:272

	if path == ":memory:" {
		return "file::memory:?" + pragmas
	}
	// A file: URI so a path containing '?' or '#' cannot be truncated into a
	// different database file by the driver's DSN split.
	u := url.URL{Scheme: "file", OmitHost: true, Path: path}
	return u.String() + "?" + pragmas
}

// Open opens (creating if needed) the SQLite database at path and migrates the
// schema. logger may be nil.
func Open(path string, logger *slog.Logger) (*Store, error) {
	db, err := sql.Open("sqlite", sqliteDSN(path))
	if err != nil {
		return nil, fmt.Errorf("open sqlite %q: %w", path, err)
	}
	if _, err := db.Exec(schema); err != nil {
		_ = db.Close()
		return nil, fmt.Errorf("migrate sqlite %q: %w", path, err)
	}
	if path != ":memory:" {
		_ = os.Chmod(path, 0o600)
	}
	for _, m := range migrations {
		if _, err := db.Exec(m); err != nil && !strings.Contains(err.Error(), "duplicate column name") {
			_ = db.Close()
			return nil, fmt.Errorf("migrate sqlite %q: %w", path, err)
		}
	}
	return &Store{db: db, logger: logger}, nil
}

// Close closes the underlying database.
func (s *Store) Close() error { return s.db.Close() }

// Record persists one request row. It is best-effort: a write failure is logged
// but never propagated, because telemetry must never break the operator's

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Verify write permission on the DB path AND its directory (SQLite creates wal/shm sidecars): chmod/prepare ~/.caveman
  2. If the file is corrupt, salvage with sqlite3 caveman.db .recover or simply delete it — telemetry history is the only loss
  3. Keep the DB on a local filesystem, never NFS/SMB shares
  4. If the schema clash comes from version drift, back up and remove the old DB so a fresh schema applies

Example fix

# before
$ caveman-proxy serve
open sqlite/migrate sqlite "/home/u/.caveman/caveman.db": attempt to write a readonly database

# after
$ mkdir -p ~/.caveman && chmod u+rwX ~/.caveman
$ rm -f ~/.caveman/caveman.db  # optional: discard corrupt/legacy file
$ caveman-proxy serve
Defensive patterns

Strategy: try-catch

Validate before calling

// Pre-flight the DB location before Open
if path != ":memory:" {
    if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
        return fmt.Errorf("db directory unwritable: %w", err)
    }
    if f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0o600); err != nil {
        return fmt.Errorf("db file unwritable: %w", err)
    } else { _ = f.Close() }
}

Try / catch

st, err := store.Open(path, logger)
if err != nil {
    if strings.Contains(err.Error(), "migrate sqlite") {
        // distinguish readonly (fix perms) vs corrupt (recover/delete) via the wrapped error
        if errors.Is(err, syscall.EROFS) || strings.Contains(err.Error(), "readonly") {
            return fmt.Errorf("%s is read-only; fix permissions", path)
        }
    }
}

Prevention

When it happens

Trigger: Opening ~/.caveman/caveman.db that is corrupt (interrupted write without WAL), on a read-only filesystem or with a directory lacking write permission (SQLite needs to create -wal/-shm files), when a non-SQLite file sits at the path, or when another SQLite engine with different locking (older NFS-style locks) holds the file.

Common situations: First run on a locked-down machine where ~/.caveman is not writable; a crashed run leaving a hot journal the driver cannot recover; moving the DB to a network share; an old DB from a pre-migration schema colliding with the base schema DDL.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/495624f1305b69fc. Report an issue: GitHub.